@akagilnc/pi-workflow-roles 0.1.3648 → 0.1.3652

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 (66) hide show
  1. package/dist/acp-host/production-host.js +2919 -1750
  2. package/dist/activation-ledger-session.js +105 -0
  3. package/dist/activation-ledger.js +139 -0
  4. package/dist/activation-trace.js +33 -0
  5. package/dist/agent-start-materials.js +11 -0
  6. package/dist/auditor-role.js +12 -0
  7. package/dist/canonical-skill-binding.js +64 -0
  8. package/dist/collector-identity.js +151 -0
  9. package/dist/collector-receipt.js +95 -0
  10. package/dist/collector-role.js +441 -0
  11. package/dist/countersign-role.js +28 -0
  12. package/dist/diarist-role.js +29 -0
  13. package/dist/doctor-role.js +74 -0
  14. package/dist/fixer-bash-seatbelt.js +13 -0
  15. package/dist/gatekeeper-pass-envelope.js +72 -0
  16. package/dist/gleaner-left-role.js +26 -0
  17. package/dist/inspector-role.js +29 -0
  18. package/dist/judge-role.js +106 -0
  19. package/dist/merger-role.js +66 -0
  20. package/dist/navigator-role.js +12 -0
  21. package/dist/notary-role.js +137 -0
  22. package/dist/oauth-keepalive.js +158 -0
  23. package/dist/package-contracts/terminating-tools.js +2 -16
  24. package/dist/packaged-role-registry.js +1 -18
  25. package/dist/pi/adapter.js +211 -0
  26. package/dist/pi/role-turn-host.js +0 -2
  27. package/dist/public-cli/instruction-seat-run.js +2 -6
  28. package/dist/public-cli/invocation.js +0 -7
  29. package/dist/public-cli/main.js +94 -191
  30. package/dist/public-cli/option-definitions.js +0 -14
  31. package/dist/public-cli/registry.js +1 -2
  32. package/dist/public-cli/run-lifecycle.js +1 -3
  33. package/dist/public-cli/settlement.js +0 -10
  34. package/dist/public-role-summons.js +0 -14
  35. package/dist/reviewer-child-executor.js +163 -70
  36. package/dist/reviewer-construction.js +2 -2
  37. package/dist/reviewer-role.js +159 -0
  38. package/dist/reviewer-settlement.js +66 -0
  39. package/dist/role-runtime.js +1394 -0
  40. package/dist/stderr-jsonl.js +26 -0
  41. package/dist/tool-execution-observation.js +141 -0
  42. package/dist/worker-role.js +343 -0
  43. package/dist/worker-submission-gates.js +305 -0
  44. package/extensions/role-runtime.ts +1 -2
  45. package/package.json +1 -1
  46. package/src/acp-host/production-host.ts +0 -1
  47. package/src/host-contracts.ts +0 -2
  48. package/src/package-contracts/terminating-tools.ts +1 -19
  49. package/src/packaged-role-registry.ts +1 -19
  50. package/src/pi/role-turn-host.ts +0 -2
  51. package/src/public-cli/cli.ts +1 -5
  52. package/src/public-cli/instruction-seat-run.ts +5 -14
  53. package/src/public-cli/invocation.ts +3 -21
  54. package/src/public-cli/option-definitions.ts +0 -16
  55. package/src/public-cli/registry.ts +1 -2
  56. package/src/public-cli/run-lifecycle.ts +0 -6
  57. package/src/public-cli/settlement.ts +0 -32
  58. package/src/public-cli/terminal.ts +0 -1
  59. package/src/public-role-summons.ts +0 -15
  60. package/src/reviewer-child-executor.ts +199 -98
  61. package/src/reviewer-construction.ts +3 -3
  62. package/src/role-runtime.ts +1 -33
  63. package/dist/package-contracts/evidence-child-output.js +0 -31
  64. package/souls/evidence-child.md +0 -1
  65. package/src/evidence-child-role.ts +0 -22
  66. package/src/package-contracts/evidence-child-output.ts +0 -46
@@ -0,0 +1,26 @@
1
+ import { Type } from "typebox";
2
+ import { openToolObject } from "./open-tool-schema.js";
3
+ import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
4
+ import { GLEANER_LEFT_OUTPUT_TOOL_NAME, validateRecordedGleanerLeftOutput, } from "./gleaner-left-contracts.js";
5
+ export { GLEANER_LEFT_ACCEPTED_TEXT, GLEANER_LEFT_OUTPUT_TOOL_NAME, } from "./gleaner-left-contracts.js";
6
+ export { validateRecordedGleanerLeftOutput };
7
+ /** 左拾遗弹章交卷形状;形状指引,非 schema 闸。 */
8
+ export const gleanerLeftOutputSchema = withInfrastructureFailureDeclaration(openToolObject(Type.Object({
9
+ status: Type.Unknown({
10
+ description: "completed — 形状指引,非 schema 闸",
11
+ }),
12
+ findings: Type.Array(Type.Object({
13
+ pointer: Type.String({ description: "文件/行指针" }),
14
+ statement: Type.String({ description: "疑点陈述" }),
15
+ }, { additionalProperties: true, description: "一条弹章" }), { description: "弹章列表;空列表合法完局" }),
16
+ })));
17
+ /**
18
+ * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
19
+ */
20
+ export const GLEANER_LEFT_TOOL_SPEC = {
21
+ name: GLEANER_LEFT_OUTPUT_TOOL_NAME,
22
+ label: "左拾遗输出",
23
+ description: "左拾遗弹章。",
24
+ promptSnippet: "左拾遗弹章",
25
+ parameters: gleanerLeftOutputSchema,
26
+ };
@@ -0,0 +1,29 @@
1
+ import { Type } from "typebox";
2
+ import { openToolObject } from "./open-tool-schema.js";
3
+ import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
4
+ import { INSPECTOR_ACCEPTED_TEXT, INSPECTOR_OUTPUT_TOOL_NAME, validateRecordedInspectorOutput, } from "./inspector-contracts.js";
5
+ export { INSPECTOR_ACCEPTED_TEXT, INSPECTOR_OUTPUT_TOOL_NAME, };
6
+ export { INSPECTOR_OUTPUT_TOOL_NAME as INSPECTOR_OUTPUT_TOOL };
7
+ export { validateRecordedInspectorOutput };
8
+ /** 察院事后察举交卷形状;形状指引,非 schema 闸。 */
9
+ export const inspectorOutputSchema = withInfrastructureFailureDeclaration(openToolObject(Type.Object({
10
+ status: Type.Unknown({
11
+ description: "pass | bounce | escalate — 形状指引,非 schema 闸",
12
+ }),
13
+ findings: Type.Unknown({
14
+ description: "随交卷留存的问题记录",
15
+ }),
16
+ reason: Type.Optional(Type.Unknown({
17
+ description: "status 为 escalate 时的上呈理由",
18
+ })),
19
+ })));
20
+ /**
21
+ * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
22
+ */
23
+ export const INSPECTOR_TOOL_SPEC = {
24
+ name: INSPECTOR_OUTPUT_TOOL_NAME,
25
+ label: "察院输出",
26
+ description: "察院终局回执,状态为 pass、bounce 或 escalate。",
27
+ promptSnippet: "察院终局回执",
28
+ parameters: inspectorOutputSchema,
29
+ };
@@ -0,0 +1,106 @@
1
+ import { stringEnum } from "./host-contracts.js";
2
+ import { Type } from "typebox";
3
+ import { disposeComplianceDecision } from "./audit-escalation.js";
4
+ import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
5
+ import { JUDGE_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, JUDGE_ACCEPTED_TEXT, JUDGE_OUTPUT_TOOL_NAME, validateAcceptedJudgeDetails, } from "./package-contracts/judge-output.js";
6
+ export { JUDGE_OUTPUT_TOOL_NAME };
7
+ export const judgeVerdictSchema = withInfrastructureFailureDeclaration(Type.Object({
8
+ judgeStatus: stringEnum(["converged", "continue", "escalate"], { description: "converged | continue | escalate — 形状指引,非 schema 闸" }),
9
+ fix: Type.Optional(Type.Object({ summary: Type.String({ minLength: 1, description: "continue 时的补救摘要" }) }, { additionalProperties: false, description: "continue 时的补救说明" })),
10
+ classes: Type.Optional(Type.Array(Type.Object({
11
+ name: Type.String({ minLength: 1 }),
12
+ owner: Type.String({ minLength: 1 }),
13
+ boundary: Type.String({ minLength: 1 }),
14
+ disposition: Type.String({ minLength: 1 }),
15
+ }, { additionalProperties: false }), { minItems: 1, description: "已裁决 finding 类及其 owner 与修理边界" })),
16
+ note: Type.Optional(Type.String({ minLength: 1, description: "可选裁决附注" })),
17
+ evidence: Type.Optional(Type.Unknown({ description: "留存的裁决证据" })),
18
+ decisionGate: Type.Optional(Type.Object({
19
+ question: Type.String({ minLength: 1 }),
20
+ options: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
21
+ }, { additionalProperties: false, description: "需人权威处置的问题与选项" })),
22
+ }, { additionalProperties: true }));
23
+ judgeVerdictSchema.required = [];
24
+ export function validateVerdict(verdict) {
25
+ return validateAcceptedJudgeDetails(verdict);
26
+ }
27
+ export function createJudgeRoleRuntime(pi, dependencies, hostActions) {
28
+ let soul;
29
+ let lifecycleRegistered = false;
30
+ return {
31
+ async activate() {
32
+ soul = (await dependencies.loadSoul()).trim();
33
+ if (soul.length === 0)
34
+ throw new Error("Judge soul is empty");
35
+ if (!lifecycleRegistered) {
36
+ lifecycleRegistered = true;
37
+ pi.registerTool({
38
+ name: JUDGE_OUTPUT_TOOL_NAME,
39
+ label: "大理寺输出",
40
+ description: "提交大理寺终局判词;受理前经审刑院审计。",
41
+ promptSnippet: "提交大理寺终局判词",
42
+ parameters: judgeVerdictSchema,
43
+ async execute(toolCallId, parameters, signal, _onUpdate, ctx) {
44
+ if (soul === undefined)
45
+ throw new Error("大理寺职分未装载");
46
+ const verdict = validateVerdict(parameters);
47
+ // Candidate verdict is already on the parent session books as this
48
+ // tool-call leaf (first-record-then-audit; run 019fea05 L61/L62).
49
+ // Gatekeeper runs after the draft is booked and before existing auditor.
50
+ await pi.requireGatekeeperPass({
51
+ context: ctx,
52
+ subject: { kind: "judge_draft" },
53
+ ...(signal === undefined ? {} : { signal }),
54
+ hostActions,
55
+ toolCallId,
56
+ });
57
+ let audit;
58
+ try {
59
+ audit = await dependencies.auditSoulCompliance(signal === undefined
60
+ ? { context: ctx }
61
+ : { context: ctx, signal });
62
+ }
63
+ catch (error) {
64
+ hostActions.failInfrastructure(error, ctx, toolCallId);
65
+ }
66
+ const acceptedDetails = verdict;
67
+ return disposeComplianceDecision(audit, {
68
+ pass: (usage) => ({
69
+ content: [{ type: "text", text: JUDGE_ACCEPTED_TEXT }],
70
+ details: acceptedDetails,
71
+ terminate: true,
72
+ ...(usage === undefined ? {} : { usage }),
73
+ }),
74
+ noReceipt: (auditNoReceipt, usageProjection) => ({
75
+ content: [{ type: "text", text: JUDGE_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }],
76
+ details: { ...acceptedDetails, auditNoReceipt },
77
+ terminate: true,
78
+ ...usageProjection,
79
+ }),
80
+ // #757: parent stands with auditor raw reply — no unreadable judgment.
81
+ received: (auditReceived, usageProjection) => ({
82
+ content: [{ type: "text", text: JUDGE_ACCEPTED_TEXT }],
83
+ details: { ...acceptedDetails, audit: auditReceived.reply },
84
+ terminate: true,
85
+ ...usageProjection,
86
+ }),
87
+ bounce: (violations) => {
88
+ throw new Error(`大理寺回执违 soul:${violations.join("; ")}`);
89
+ },
90
+ escalate: (result) => result,
91
+ },
92
+ // #380: escalate deliveredOutput must carry the same mechanical projection.
93
+ acceptedDetails);
94
+ },
95
+ });
96
+ pi.on("before_agent_start", (event) => {
97
+ if (soul === undefined)
98
+ throw new Error("大理寺职分未装载");
99
+ return {
100
+ systemPrompt: `${event.systemPrompt}\n\n<judge_soul>\n${soul}\n</judge_soul>`,
101
+ };
102
+ });
103
+ }
104
+ },
105
+ };
106
+ }
@@ -0,0 +1,66 @@
1
+ import { MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, mergerOutputSchema, validateMergerInput, validateMergerOutput } from "./merger-contracts.js";
2
+ import { exactUtf8 } from "./exact-utf8.js";
3
+ import { isFullGitObjectId } from "./git-object-id.js";
4
+ export { MERGER_OUTPUT_TOOL_NAME };
5
+ export const MERGER_INPUT_FLAG = { name: "ak-merger-input", definition: { description: "Path to an immutable digest-bound Merger v1 input JSON file", type: "string" } };
6
+ function same(a, b) { return a.length === b.length && a.every((value, i) => value === b[i]); }
7
+ function materialText(input, key) { return exactUtf8(Buffer.from(input.materials[key].bytesBase64, "base64"), `Merger ${key}`); }
8
+ export function createMergerRoleRuntime(pi, dependencies, host) {
9
+ let activation;
10
+ let registered = false;
11
+ let accepted = false;
12
+ pi.registerFlag(MERGER_INPUT_FLAG.name, MERGER_INPUT_FLAG.definition);
13
+ return { async activate() {
14
+ const path = pi.getFlag(MERGER_INPUT_FLAG.name);
15
+ if (typeof path !== "string" || path.trim().length === 0)
16
+ throw new Error("Merger requires --ak-merger-input");
17
+ const soul = (await dependencies.loadSoul()).trim();
18
+ if (!soul)
19
+ throw new Error("Merger soul is empty");
20
+ const input = validateMergerInput(await dependencies.loadInput(path));
21
+ const state = await dependencies.gitState.activeMerge();
22
+ 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)
23
+ throw new Error("Merger activation rejected repository parent, merge, automatic-result, or complete conflict-set drift");
24
+ activation = { soul, input, automaticMergeTreeId: state.automaticMergeTreeId };
25
+ accepted = false;
26
+ if (!registered) {
27
+ registered = true;
28
+ pi.registerTool({ name: MERGER_OUTPUT_TOOL_NAME, label: "合并输出", description: "提交合并结果;输出分支为 completed 与 escalate;基础设施及 Git 失败由 abort 通道承接。", promptSnippet: "提交合并结果", parameters: mergerOutputSchema,
29
+ async execute(id, params, _signal, _update, ctx) {
30
+ if (!activation)
31
+ throw new Error("校书郎未激活");
32
+ if (accepted)
33
+ throw new Error("合并回执已受理");
34
+ let output;
35
+ try {
36
+ output = validateMergerOutput(params, activation.input.attemptId);
37
+ }
38
+ catch (error) {
39
+ host.failInfrastructure(error, ctx, id);
40
+ }
41
+ if (output.status === "completed") {
42
+ let state;
43
+ try {
44
+ state = await dependencies.gitState.completedMerge(output.mergeCommitId, activation.automaticMergeTreeId);
45
+ }
46
+ catch (error) {
47
+ host.failInfrastructure(error, ctx, id);
48
+ }
49
+ const scope = new Set(activation.input.resolutionScope);
50
+ 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)))
51
+ host.failInfrastructure(new Error("Merger completed-state verification failed"), ctx, id);
52
+ }
53
+ accepted = true;
54
+ const acceptedDetails = output;
55
+ return { content: [{ type: "text", text: MERGER_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true };
56
+ } });
57
+ pi.on("before_agent_start", (event) => { if (!activation)
58
+ 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>` }; });
59
+ }
60
+ // Host-neutral AK output only (ADR 0018 fail-closed); no host-builtin name hard-require.
61
+ const all = pi.getAllTools().map(tool => tool.name);
62
+ if (all.filter(item => item === MERGER_OUTPUT_TOOL_NAME).length !== 1) {
63
+ throw new Error(`Merger required tool collision or missing: ${MERGER_OUTPUT_TOOL_NAME}`);
64
+ }
65
+ } };
66
+ }
@@ -0,0 +1,12 @@
1
+ import { NAVIGATOR_OUTPUT_TOOL_NAME, navigatorOutputSchema } from "./package-contracts/navigator-output.js";
2
+ /**
3
+ * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
4
+ * Candidates share the route-advice shape the attendance prepare tool owns.
5
+ */
6
+ export const NAVIGATOR_TOOL_SPEC = {
7
+ name: NAVIGATOR_OUTPUT_TOOL_NAME,
8
+ label: "游奕使建议",
9
+ description: "游奕使终局回执:排好序的下一步角色路线建议。",
10
+ promptSnippet: "游奕使建议",
11
+ parameters: navigatorOutputSchema,
12
+ };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Public Notary role runtime — direct officer seat (not through Gatekeeper province).
3
+ * Caller supplies only a source-run locator; Notary self-fetches authoritative materials.
4
+ * Ticket flag register/read/session bind is envelope-owned (ADR 0018 / #582).
5
+ * This module keeps evidence assembly + projection only.
6
+ */
7
+ import { NOTARY_ACCEPTED_TEXT, NOTARY_OUTPUT_TOOL_NAME, NOTARY_SOURCE_RUN_FLAG, NOTARY_TICKET_FLAG, notaryOutputSchema, projectLawfulNotaryOutput, retainNotarySubmission, } from "./notary-contracts.js";
8
+ export { NOTARY_ACCEPTED_TEXT, NOTARY_OUTPUT_TOOL_NAME, NOTARY_SOURCE_RUN_FLAG, NOTARY_TICKET_FLAG, };
9
+ /** Optional ticket flag: absent/blank = unbound; non-empty invalid = honest fail. */
10
+ export function readNotaryTicketFlag(flag) {
11
+ if (flag === undefined)
12
+ return undefined;
13
+ if (typeof flag !== "string") {
14
+ throw new Error("Notary ak-notary-ticket-number is present but not a safe positive integer string");
15
+ }
16
+ if (flag.trim() === "")
17
+ return undefined;
18
+ const n = Number(flag);
19
+ if (!Number.isSafeInteger(n) || n < 1) {
20
+ throw new Error("Notary ak-notary-ticket-number is present but not a safe positive integer string");
21
+ }
22
+ return n;
23
+ }
24
+ /**
25
+ * Typed session bound (locator + optional ticket).
26
+ * Sole production projection for agent-start material + session custom entry.
27
+ */
28
+ export function projectNotarySessionBound(input) {
29
+ return {
30
+ sourceRun: input.sourceRun,
31
+ ...(input.ticketNumber === undefined ? {} : { ticketNumber: input.ticketNumber }),
32
+ };
33
+ }
34
+ /**
35
+ * Session custom-entry type for typed notary bound (written by shared envelope).
36
+ * Role module only projects; lifecycle write is envelope-owned (ADR 0018).
37
+ */
38
+ export const NOTARY_SESSION_BOUND_ENTRY = "notary-session-bound";
39
+ /**
40
+ * Project notary bound from host flags (envelope consumption seam).
41
+ * undefined when source-run flag absent/blank.
42
+ * Ticket invalid → throws (honest fail; envelope owns the read).
43
+ */
44
+ export function projectNotaryBoundFromFlags(getFlag) {
45
+ const path = getFlag(NOTARY_SOURCE_RUN_FLAG.name);
46
+ if (typeof path !== "string" || path.trim() === "")
47
+ return undefined;
48
+ const ticketNumber = readNotaryTicketFlag(getFlag(NOTARY_TICKET_FLAG.name));
49
+ return {
50
+ sourceRunPath: path,
51
+ ...(ticketNumber === undefined ? {} : { ticketNumber }),
52
+ };
53
+ }
54
+ /** Assemble systemPrompt body from base + soul only (NO embedded bound JSON).
55
+ * The typed session bound travels separately as `readingMaterial`; adapters fold
56
+ * it into the provider-visible prompt at the send boundary. */
57
+ export function assembleNotaryAgentStartPrompt(input) {
58
+ return `${input.baseSystemPrompt}\n\n<notary_soul>\n${input.soul}\n</notary_soul>`;
59
+ }
60
+ export function createNotaryRoleRuntime(pi, dependencies, host) {
61
+ let activation;
62
+ let registered = false;
63
+ // Baseline source-run flag registration (pre-#582). Ticket flag is envelope-owned.
64
+ pi.registerFlag(NOTARY_SOURCE_RUN_FLAG.name, NOTARY_SOURCE_RUN_FLAG.definition);
65
+ return {
66
+ /**
67
+ * @param admitted Optional ticket from envelope flag decode (ADR 0018).
68
+ * Role module never getFlag's the ticket — envelope owns that read.
69
+ */
70
+ async activate(admitted) {
71
+ const path = pi.getFlag(NOTARY_SOURCE_RUN_FLAG.name);
72
+ if (typeof path !== "string" || path.trim() === "") {
73
+ throw new Error("Notary requires --ak-notary-source-run");
74
+ }
75
+ const soul = (await dependencies.loadSoul()).trim();
76
+ if (soul.length === 0)
77
+ throw new Error("Notary soul is empty");
78
+ const sourceRun = await dependencies.loadSourceRunLocator(path);
79
+ activation = {
80
+ soul,
81
+ sourceRun,
82
+ ...(admitted?.ticketNumber === undefined
83
+ ? {}
84
+ : { ticketNumber: admitted.ticketNumber }),
85
+ };
86
+ if (!registered) {
87
+ registered = true;
88
+ pi.registerTool({
89
+ name: NOTARY_OUTPUT_TOOL_NAME,
90
+ label: "符宝郎输出",
91
+ description: "提交引文保真与票面对齐的 typed pass/bounce/escalate 决议。",
92
+ promptSnippet: "提交符宝郎决议",
93
+ parameters: notaryOutputSchema,
94
+ async execute(toolCallId, parameters, _signal, _onUpdate, ctx) {
95
+ if (activation === undefined) {
96
+ throw new Error("符宝郎未激活");
97
+ }
98
+ // Unique submission + terminate only. Shape is not an admission gate
99
+ // (第 0 条 / ADR 0055): lawful pass/bounce/escalate projected; else params as-is.
100
+ // #541 infra declaration + sole-final barrier are ledger-owned (#575).
101
+ const lawful = projectLawfulNotaryOutput(parameters);
102
+ const details = lawful ?? retainNotarySubmission(parameters);
103
+ return {
104
+ content: [{ type: "text", text: NOTARY_ACCEPTED_TEXT }],
105
+ details,
106
+ terminate: true,
107
+ };
108
+ },
109
+ });
110
+ // Evidence assembly only (ADR 0018): soul + bound projection.
111
+ // Ticket value is envelope-admitted at activate — not re-read from flags here.
112
+ pi.on("before_agent_start", (event) => {
113
+ if (activation === undefined) {
114
+ throw new Error("符宝郎未激活");
115
+ }
116
+ return {
117
+ systemPrompt: assembleNotaryAgentStartPrompt({
118
+ baseSystemPrompt: event.systemPrompt,
119
+ soul: activation.soul,
120
+ }),
121
+ readingMaterial: projectNotarySessionBound({
122
+ sourceRun: activation.sourceRun,
123
+ ...(activation.ticketNumber === undefined
124
+ ? {}
125
+ : { ticketNumber: activation.ticketNumber }),
126
+ }),
127
+ };
128
+ });
129
+ }
130
+ // Evidence role: keep Pi default tools + notary output (ADR 0064 unrestricted).
131
+ const all = pi.getAllTools().map((tool) => tool.name);
132
+ if (all.filter((name) => name === NOTARY_OUTPUT_TOOL_NAME).length !== 1) {
133
+ throw new Error(`Notary required tool collision or missing: ${NOTARY_OUTPUT_TOOL_NAME}`);
134
+ }
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Extension-side OAuth credential keepalive (#351).
3
+ *
4
+ * Periodically calls the public `ctx.modelRegistry.refresh` facade so expired
5
+ * OAuth tokens (e.g. kimi-coding 900s TTL) are renewed during long sessions.
6
+ * Does not read auth.json, touch pi-ai private APIs, or reuse the 183s idle gate.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ export const DEFAULT_OAUTH_KEEPALIVE_PROVIDERS = ["kimi-coding"];
12
+ export const OAUTH_KEEPALIVE_INTERVAL_MS = 60_000;
13
+ /** Persistent extension setting filename under PI_CODING_AGENT_DIR / ~/.pi/agent. */
14
+ export const OAUTH_KEEPALIVE_SETTING_FILENAME = "oauth-keepalive.json";
15
+ /** Path to the static provider-list extension setting (default ["kimi-coding"]). */
16
+ export function oauthKeepaliveSettingPath() {
17
+ return join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), OAUTH_KEEPALIVE_SETTING_FILENAME);
18
+ }
19
+ function isExactRecord(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ /**
23
+ * Read the static keepalive provider list from the extension setting file.
24
+ * Missing file → default ["kimi-coding"]. Malformed content fails closed.
25
+ * Production extension root is the sole caller that feeds this into keepalive.
26
+ */
27
+ export function readOAuthKeepaliveProviders(path = oauthKeepaliveSettingPath()) {
28
+ try {
29
+ const raw = JSON.parse(readFileSync(path, "utf8"));
30
+ if (!isExactRecord(raw) || !Array.isArray(raw.providers)) {
31
+ throw new Error("OAuth keepalive setting is malformed: expected { providers: string[] }");
32
+ }
33
+ const providers = raw.providers.map((id, index) => {
34
+ if (typeof id !== "string" || id.trim() === "") {
35
+ throw new Error(`OAuth keepalive setting providers[${index}] must be a non-empty string`);
36
+ }
37
+ // Accept via trim-non-empty; return the normalized id so refresh can hit real providers.
38
+ return id.trim();
39
+ });
40
+ return Object.freeze(providers);
41
+ }
42
+ catch (error) {
43
+ // Absent optional setting uses the documented default; all other causes propagate.
44
+ if (error instanceof Error &&
45
+ "code" in error &&
46
+ error.code === "ENOENT") {
47
+ return DEFAULT_OAUTH_KEEPALIVE_PROVIDERS;
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+ const defaultScheduler = {
53
+ every(ms, tick) {
54
+ const timer = setInterval(tick, ms);
55
+ // Allow the process to exit if a caller forgets stop (same discipline as trajectory).
56
+ timer.unref?.();
57
+ return () => clearInterval(timer);
58
+ },
59
+ };
60
+ function errorClassName(error) {
61
+ if (error instanceof Error && error.name.trim() !== "")
62
+ return error.name;
63
+ if (error !== null && typeof error === "object" && "name" in error) {
64
+ const name = error.name;
65
+ if (typeof name === "string" && name.trim() !== "")
66
+ return name;
67
+ }
68
+ return "Error";
69
+ }
70
+ function formatWarning(providerId, error) {
71
+ const cls = errorClassName(error);
72
+ const detail = error instanceof Error
73
+ ? error.message
74
+ : typeof error === "string"
75
+ ? error
76
+ : String(error);
77
+ return `oauth-keepalive: refresh failed for provider ${providerId} (${cls}): ${detail}`;
78
+ }
79
+ function emitWarning(ctx, providerId, error) {
80
+ const text = formatWarning(providerId, error);
81
+ // Exactly one visible surface per failure: prefer ui.notify when present;
82
+ // console.warn only when notify is unavailable (print/json / headless)
83
+ // or when notify itself throws (still one surface; carry the notify cause).
84
+ const notify = ctx.ui?.notify;
85
+ if (typeof notify === "function") {
86
+ try {
87
+ notify(text, "warning");
88
+ }
89
+ catch (notifyError) {
90
+ // Notify threw: fall back once so refresh failure stays visible.
91
+ console.warn(text, notifyError);
92
+ }
93
+ return;
94
+ }
95
+ console.warn(text);
96
+ }
97
+ /**
98
+ * Session-lifecycle owner for periodic OAuth refresh via the public modelRegistry facade.
99
+ * Single-flight: overlapping ticks are skipped. stop() cancels the interval and aborts in-flight work.
100
+ */
101
+ export function createOAuthKeepalive(options = {}) {
102
+ const providers = Object.freeze((options.providers ?? DEFAULT_OAUTH_KEEPALIVE_PROVIDERS).map((id) => id));
103
+ const intervalMs = options.intervalMs ?? OAUTH_KEEPALIVE_INTERVAL_MS;
104
+ const scheduler = options.scheduler ?? defaultScheduler;
105
+ let cancel;
106
+ let abortController;
107
+ /** Bare single-flight gate; survives stop/start until the in-flight call settles. */
108
+ let inFlight = false;
109
+ const stop = () => {
110
+ cancel?.();
111
+ cancel = undefined;
112
+ abortController?.abort();
113
+ abortController = undefined;
114
+ // Do not clear unsettled inFlight — single-flight is continuous across stop/start.
115
+ };
116
+ const runTick = async (ctx) => {
117
+ if (inFlight)
118
+ return;
119
+ const ac = abortController;
120
+ if (ac === undefined || ac.signal.aborted)
121
+ return;
122
+ inFlight = true;
123
+ try {
124
+ const result = await ctx.modelRegistry.refresh({
125
+ providers: [...providers],
126
+ allowNetwork: true,
127
+ signal: ac.signal,
128
+ });
129
+ if (ac.signal.aborted || result.aborted)
130
+ return;
131
+ for (const [providerId, error] of result.errors) {
132
+ emitWarning(ctx, providerId, error);
133
+ }
134
+ }
135
+ catch (error) {
136
+ if (ac.signal.aborted)
137
+ return;
138
+ // Whole-call failure: one warning per configured provider (ticket error surface).
139
+ for (const providerId of providers) {
140
+ emitWarning(ctx, providerId, error);
141
+ }
142
+ }
143
+ finally {
144
+ inFlight = false;
145
+ }
146
+ };
147
+ return {
148
+ start(ctx) {
149
+ // Reload / re-start: tear down any prior interval before hanging a new one.
150
+ stop();
151
+ abortController = new AbortController();
152
+ cancel = scheduler.every(intervalMs, () => {
153
+ void runTick(ctx);
154
+ });
155
+ },
156
+ stop,
157
+ };
158
+ }
@@ -11,7 +11,6 @@ import { DOCTOR_ACCEPTED_TEXT, DOCTOR_OUTPUT_TOOL_NAME, validateDoctorSubmission
11
11
  import { GATEKEEPER_ACCEPTED_TEXT, GATEKEEPER_OUTPUT_TOOL_NAME, validateRecordedGatekeeperOutput } from "./gatekeeper-output.js";
12
12
  import { NAVIGATOR_ACCEPTED_TEXT, NAVIGATOR_OUTPUT_TOOL_NAME, validateRecordedNavigatorOutput } from "./navigator-output.js";
13
13
  import { AUDITOR_ACCEPTED_TEXT, AUDITOR_OUTPUT_TOOL_NAME, validateRecordedAuditorOutput } from "./auditor-output.js";
14
- import { EVIDENCE_CHILD_ACCEPTED_TEXT, EVIDENCE_CHILD_OUTPUT_TOOL_NAME, validateRecordedEvidenceChildOutput } from "./evidence-child-output.js";
15
14
  import { MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, validateMergerOutput } from "../merger-contracts.js";
16
15
  import { NOTARY_ACCEPTED_TEXT, NOTARY_OUTPUT_TOOL_NAME, validateRecordedNotaryOutput } from "../notary-contracts.js";
17
16
  import { COUNTERSIGN_ACCEPTED_TEXT, COUNTERSIGN_OUTPUT_TOOL_NAME, validateRecordedCountersignOutput } from "../countersign-contracts.js";
@@ -19,7 +18,7 @@ import { GLEANER_LEFT_ACCEPTED_TEXT, GLEANER_LEFT_OUTPUT_TOOL_NAME, validateReco
19
18
  import { INSPECTOR_ACCEPTED_TEXT, INSPECTOR_OUTPUT_TOOL_NAME, validateRecordedInspectorOutput } from "../inspector-contracts.js";
20
19
  import { DIARIST_ACCEPTED_TEXT, DIARIST_OUTPUT_TOOL_NAME, validateRecordedDiaristOutput } from "../diarist-contracts.js";
21
20
  import { CODER_ACCEPTED_TEXT, CODER_OUTPUT_TOOL_NAME, FIXER_ACCEPTED_TEXT, FIXER_OUTPUT_TOOL_NAME, validateAcceptedWorkerDetails, } from "./worker-output.js";
22
- export { CODER_ACCEPTED_TEXT, CODER_OUTPUT_TOOL_NAME, COLLECTOR_ACCEPTED_TEXT, COLLECTOR_OUTPUT_TOOL, FIXER_ACCEPTED_TEXT, FIXER_OUTPUT_TOOL_NAME, JUDGE_ACCEPTED_TEXT, JUDGE_OUTPUT_TOOL_NAME, REVIEWER_ACCEPTED_TEXT, REVIEWER_OUTPUT_TOOL_NAME, MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, validateAcceptedCollectorReceipt, validateAcceptedJudgeDetails, projectReviewerIntentToReceipt, validateReviewerIntent, validateRuntimeReviewerReceipt, validateAcceptedWorkerDetails, validateDoctorSubmissionShape, validateRecordedDoctorOutput, validateMergerOutput, validateRecordedNotaryOutput, validateRecordedCountersignOutput, validateRecordedGleanerLeftOutput, validateRecordedInspectorOutput, validateRecordedGatekeeperOutput, validateRecordedNavigatorOutput, validateRecordedAuditorOutput, validateRecordedEvidenceChildOutput, validateRecordedDiaristOutput, };
21
+ export { CODER_ACCEPTED_TEXT, CODER_OUTPUT_TOOL_NAME, COLLECTOR_ACCEPTED_TEXT, COLLECTOR_OUTPUT_TOOL, FIXER_ACCEPTED_TEXT, FIXER_OUTPUT_TOOL_NAME, JUDGE_ACCEPTED_TEXT, JUDGE_OUTPUT_TOOL_NAME, REVIEWER_ACCEPTED_TEXT, REVIEWER_OUTPUT_TOOL_NAME, MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, validateAcceptedCollectorReceipt, validateAcceptedJudgeDetails, projectReviewerIntentToReceipt, validateReviewerIntent, validateRuntimeReviewerReceipt, validateAcceptedWorkerDetails, validateDoctorSubmissionShape, validateRecordedDoctorOutput, validateMergerOutput, validateRecordedNotaryOutput, validateRecordedCountersignOutput, validateRecordedGleanerLeftOutput, validateRecordedInspectorOutput, validateRecordedGatekeeperOutput, validateRecordedNavigatorOutput, validateRecordedAuditorOutput, validateRecordedDiaristOutput, };
23
22
  export const TERMINATING_TOOL_NAMES = [
24
23
  CODER_OUTPUT_TOOL_NAME,
25
24
  FIXER_OUTPUT_TOOL_NAME,
@@ -35,7 +34,6 @@ export const TERMINATING_TOOL_NAMES = [
35
34
  GATEKEEPER_OUTPUT_TOOL_NAME,
36
35
  NAVIGATOR_OUTPUT_TOOL_NAME,
37
36
  AUDITOR_OUTPUT_TOOL_NAME,
38
- EVIDENCE_CHILD_OUTPUT_TOOL_NAME,
39
37
  DIARIST_OUTPUT_TOOL_NAME,
40
38
  ];
41
39
  export function isTerminatingToolName(name) {
@@ -71,8 +69,6 @@ export function acceptedTextFor(toolName) {
71
69
  return NAVIGATOR_ACCEPTED_TEXT;
72
70
  case AUDITOR_OUTPUT_TOOL_NAME:
73
71
  return AUDITOR_ACCEPTED_TEXT;
74
- case EVIDENCE_CHILD_OUTPUT_TOOL_NAME:
75
- return EVIDENCE_CHILD_ACCEPTED_TEXT;
76
72
  case DIARIST_OUTPUT_TOOL_NAME:
77
73
  return DIARIST_ACCEPTED_TEXT;
78
74
  }
@@ -127,20 +123,14 @@ export function validateAcceptedDetails(toolName, details) {
127
123
  [GATEKEEPER_OUTPUT_TOOL_NAME]: ["dispatch", "pass"],
128
124
  [NAVIGATOR_OUTPUT_TOOL_NAME]: ["advice"],
129
125
  [AUDITOR_OUTPUT_TOOL_NAME]: ["pass", "bounce", "escalate"],
130
- [EVIDENCE_CHILD_OUTPUT_TOOL_NAME]: [],
131
126
  [DIARIST_OUTPUT_TOOL_NAME]: ["completed"],
132
127
  };
133
128
  const collectorDiscriminator = toolName === COLLECTOR_OUTPUT_TOOL && Array.isArray(candidate?.groups);
134
- // Evidence-child: any object is a candidate body — no report-field presence gate (#675 / ADR 0055).
135
- const evidenceChildDiscriminator = toolName === EVIDENCE_CHILD_OUTPUT_TOOL_NAME
136
- && candidate !== null
137
- && typeof candidate === "object"
138
- && !Array.isArray(candidate);
139
129
  const baseDiscriminator = discriminator;
140
130
  const runtimeBindingMissing = (toolName === DOCTOR_OUTPUT_TOOL_NAME && baseDiscriminator === "completed" && !(candidate?.cost !== null && typeof candidate?.cost === "object")) ||
141
131
  (toolName === REVIEWER_OUTPUT_TOOL_NAME && candidate?.version !== 2);
142
132
  if (runtimeBindingMissing ||
143
- (!collectorDiscriminator && !evidenceChildDiscriminator && (typeof discriminator !== "string" || !lawfulStatuses[toolName].includes(baseDiscriminator)))) {
133
+ (!collectorDiscriminator && (typeof discriminator !== "string" || !lawfulStatuses[toolName].includes(baseDiscriminator)))) {
144
134
  throw new AcceptedDetailsContractError("terminating receipt has no recognized execution discriminator");
145
135
  }
146
136
  try {
@@ -173,8 +163,6 @@ export function validateAcceptedDetails(toolName, details) {
173
163
  return validateRecordedNavigatorOutput(details);
174
164
  case AUDITOR_OUTPUT_TOOL_NAME:
175
165
  return validateRecordedAuditorOutput(details);
176
- case EVIDENCE_CHILD_OUTPUT_TOOL_NAME:
177
- return validateRecordedEvidenceChildOutput(details);
178
166
  case DIARIST_OUTPUT_TOOL_NAME:
179
167
  return validateRecordedDiaristOutput(details);
180
168
  }
@@ -231,8 +219,6 @@ export function acceptedFacts(toolName, details) {
231
219
  case NAVIGATOR_OUTPUT_TOOL_NAME:
232
220
  case AUDITOR_OUTPUT_TOOL_NAME:
233
221
  return { status: details.status };
234
- case EVIDENCE_CHILD_OUTPUT_TOOL_NAME:
235
- return { status: "report" };
236
222
  case DIARIST_OUTPUT_TOOL_NAME: return { status: details.status };
237
223
  case JUDGE_OUTPUT_TOOL_NAME: return { status: details.judgeStatus };
238
224
  case COUNTERSIGN_OUTPUT_TOOL_NAME: return { status: details.countersignStatus };