@akagilnc/pi-workflow-roles 0.1.1751
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +104 -0
- package/README.zh-CN.md +133 -0
- package/THIRD_PARTY_NOTICES.md +60 -0
- package/dist/activation-ledger-git.js +68 -0
- package/dist/activation-ledger-session.js +120 -0
- package/dist/activation-ledger-topology.js +239 -0
- package/dist/activation-reconciliation.js +61 -0
- package/dist/audit-escalation.js +108 -0
- package/dist/auditor-dossier-tool.js +35 -0
- package/dist/canonical-json.js +78 -0
- package/dist/compliance-transport.js +77 -0
- package/dist/doctor-contracts.js +172 -0
- package/dist/dossier-resolution.js +103 -0
- package/dist/evidence-child-executor.js +661 -0
- package/dist/exact-utf8.js +12 -0
- package/dist/git-object-id.js +7 -0
- package/dist/in-process-session.js +50 -0
- package/dist/merger-contracts.js +76 -0
- package/dist/navigator-attendance.js +995 -0
- package/dist/navigator-invocation-identity.js +220 -0
- package/dist/open-tool-schema.js +39 -0
- package/dist/package-contracts/collector-output.js +50 -0
- package/dist/package-contracts/fixer-output.js +72 -0
- package/dist/package-contracts/fixer-packet.js +77 -0
- package/dist/package-contracts/judge-output.js +17 -0
- package/dist/package-contracts/reviewer-output.js +82 -0
- package/dist/package-contracts/terminating-tools.js +173 -0
- package/dist/package-contracts/worker-output.js +13 -0
- package/dist/package-owned-tool-idle.js +104 -0
- package/dist/packaged-role-registry.js +34 -0
- package/dist/public-cli/main.js +23867 -0
- package/dist/public-command-renderer.js +20 -0
- package/dist/reviewer-agent.js +93 -0
- package/dist/reviewer-child-executor.js +23 -0
- package/dist/reviewer-construction.js +95 -0
- package/dist/reviewer-dispatch.js +77 -0
- package/dist/reviewer-execution-ledger.js +160 -0
- package/dist/reviewer-failure-diagnostic.js +17 -0
- package/dist/reviewer-git-snapshot.js +38 -0
- package/dist/reviewer-pinned-git.js +146 -0
- package/dist/reviewer-preflight-error.js +15 -0
- package/dist/reviewer-prompt-identity.js +10 -0
- package/dist/reviewer-scope-prompt.js +21 -0
- package/dist/reviewer-workspace.js +151 -0
- package/dist/sha256.js +5 -0
- package/dist/sitian-record-entry.js +33 -0
- package/dist/stderr-jsonl.js +26 -0
- package/dist/stream-idle-guard.js +75 -0
- package/dist/tool-execution-observation.js +141 -0
- package/dist/uuidv7.js +21 -0
- package/dist/work-subject-identity.js +53 -0
- package/extensions/role-runtime.ts +303 -0
- package/package.json +69 -0
- package/packets/fixer-prerequisites.json +6 -0
- package/packets/fixer-repair.md +5 -0
- package/packets/judge-apply.md +77 -0
- package/packets/judge-authority.md +64 -0
- package/packets/judge-plan.md +55 -0
- package/packets/judge-review.md +49 -0
- package/packets/judge-submission.md +34 -0
- package/resources/methods/code-review/SKILL.md +92 -0
- package/resources/methods/code-review/agents/openai.yaml +3 -0
- package/resources/methods/code-review/provenance.json +26 -0
- package/resources/methods/diagnosing-bugs/SKILL.md +134 -0
- package/resources/methods/diagnosing-bugs/agents/openai.yaml +3 -0
- package/resources/methods/diagnosing-bugs/provenance.json +31 -0
- package/resources/methods/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/resources/methods/resolving-merge-conflicts/SKILL.md +14 -0
- package/resources/methods/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/resources/methods/resolving-merge-conflicts/provenance.json +26 -0
- package/resources/methods/tdd/SKILL.md +38 -0
- package/resources/methods/tdd/agents/openai.yaml +3 -0
- package/resources/methods/tdd/mocking.md +59 -0
- package/resources/methods/tdd/provenance.json +36 -0
- package/resources/methods/tdd/tests.md +77 -0
- package/resources/navigator-route-playbook.md +32 -0
- package/schemas/tool-execution-observation.schema.json +107 -0
- package/scripts/build-package.mjs +65 -0
- package/scripts/generate-tool-execution-observation-schema.ts +7 -0
- package/souls/coder.md +10 -0
- package/souls/collector.md +11 -0
- package/souls/doctor-auditor.md +23 -0
- package/souls/doctor.md +8 -0
- package/souls/fixer-auditor.md +33 -0
- package/souls/fixer.md +13 -0
- package/souls/judge-auditor.md +33 -0
- package/souls/judge.md +74 -0
- package/souls/merger.md +5 -0
- package/souls/navigator.md +5 -0
- package/souls/reviewer-auditor.md +25 -0
- package/souls/reviewer.md +11 -0
- package/src/activation-ledger-git.ts +96 -0
- package/src/activation-ledger-session.ts +188 -0
- package/src/activation-ledger-topology.ts +301 -0
- package/src/activation-ledger.ts +240 -0
- package/src/activation-reconciliation.ts +163 -0
- package/src/activation-trace.ts +38 -0
- package/src/audit-escalation.ts +177 -0
- package/src/auditor-dossier-tool.ts +48 -0
- package/src/auditor-soul.ts +28 -0
- package/src/canonical-json.ts +74 -0
- package/src/canonical-skill-binding.ts +107 -0
- package/src/collector-config.ts +89 -0
- package/src/collector-evidence.ts +461 -0
- package/src/collector-github.ts +656 -0
- package/src/collector-identity.ts +161 -0
- package/src/collector-ledger.ts +827 -0
- package/src/collector-receipt.ts +87 -0
- package/src/collector-role.ts +592 -0
- package/src/collector-tool-schemas.ts +19 -0
- package/src/compliance-transport.ts +130 -0
- package/src/doctor-auditor.ts +53 -0
- package/src/doctor-contracts.ts +166 -0
- package/src/doctor-evidence.ts +47 -0
- package/src/doctor-role.ts +18 -0
- package/src/dossier-resolution.ts +137 -0
- package/src/evidence-child-executor.ts +775 -0
- package/src/exact-utf8.ts +9 -0
- package/src/factory-board.ts +1822 -0
- package/src/git-object-id.ts +11 -0
- package/src/human-format.ts +65 -0
- package/src/in-process-session.ts +78 -0
- package/src/judge-auditor.ts +55 -0
- package/src/judge-recording-anti-forge.ts +53 -0
- package/src/judge-role.ts +160 -0
- package/src/merger-contracts.ts +71 -0
- package/src/merger-git-state.ts +76 -0
- package/src/merger-role.ts +60 -0
- package/src/navigator-attendance.ts +1254 -0
- package/src/navigator-invocation-identity.ts +446 -0
- package/src/open-tool-schema.ts +46 -0
- package/src/package-contracts/collector-output.ts +109 -0
- package/src/package-contracts/fixer-output.ts +81 -0
- package/src/package-contracts/fixer-packet.ts +93 -0
- package/src/package-contracts/judge-output.ts +39 -0
- package/src/package-contracts/reviewer-output.ts +115 -0
- package/src/package-contracts/terminating-tools.ts +259 -0
- package/src/package-contracts/worker-output.ts +36 -0
- package/src/package-owned-tool-idle.ts +134 -0
- package/src/package-resources/method-skill-binding.ts +87 -0
- package/src/package-resources/method-skill.ts +358 -0
- package/src/packaged-role-registry.ts +36 -0
- package/src/public-cli/cli-errors.ts +11 -0
- package/src/public-cli/cli-io.ts +4 -0
- package/src/public-cli/cli.ts +912 -0
- package/src/public-cli/coder-run.ts +575 -0
- package/src/public-cli/collector-run.ts +375 -0
- package/src/public-cli/command-renderer.ts +8 -0
- package/src/public-cli/config.ts +346 -0
- package/src/public-cli/doctor-run.ts +355 -0
- package/src/public-cli/explicit-internal.ts +274 -0
- package/src/public-cli/fixer-run.ts +587 -0
- package/src/public-cli/host-pi-runtime.ts +112 -0
- package/src/public-cli/invocation.ts +1958 -0
- package/src/public-cli/judge-run.ts +507 -0
- package/src/public-cli/main.ts +15 -0
- package/src/public-cli/merger-run.ts +681 -0
- package/src/public-cli/public-run-credentials.ts +71 -0
- package/src/public-cli/registry.ts +153 -0
- package/src/public-cli/reviewer-run.ts +561 -0
- package/src/public-cli/run-lifecycle.ts +884 -0
- package/src/public-cli/settlement.ts +3765 -0
- package/src/public-cli/terminal.ts +325 -0
- package/src/public-command-renderer.ts +43 -0
- package/src/reviewer-agent.ts +94 -0
- package/src/reviewer-auditor.ts +53 -0
- package/src/reviewer-child-executor.ts +31 -0
- package/src/reviewer-construction.ts +137 -0
- package/src/reviewer-dispatch.ts +94 -0
- package/src/reviewer-execution-ledger.ts +206 -0
- package/src/reviewer-failure-diagnostic.ts +18 -0
- package/src/reviewer-git-snapshot.ts +53 -0
- package/src/reviewer-pinned-git.ts +144 -0
- package/src/reviewer-preflight-error.ts +14 -0
- package/src/reviewer-prompt-identity.ts +17 -0
- package/src/reviewer-role.ts +193 -0
- package/src/reviewer-scope-prompt.ts +24 -0
- package/src/reviewer-settlement.ts +63 -0
- package/src/reviewer-workspace.ts +111 -0
- package/src/role-runtime.ts +884 -0
- package/src/sha256.ts +6 -0
- package/src/sitian-record-entry.ts +57 -0
- package/src/stderr-jsonl.ts +28 -0
- package/src/stream-idle-guard.ts +98 -0
- package/src/ticket-snapshot.ts +662 -0
- package/src/ticket-trajectory.ts +1000 -0
- package/src/tool-execution-observation.ts +168 -0
- package/src/uuidv7.ts +1 -0
- package/src/work-subject-identity.ts +94 -0
- package/src/worker-role.ts +434 -0
- package/src/worker-submission-gates.ts +225 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type GitObjectFormat = "sha1" | "sha256";
|
|
2
|
+
|
|
3
|
+
export const FULL_GIT_OBJECT_ID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
4
|
+
|
|
5
|
+
export function isFullGitObjectId(value: unknown): value is string {
|
|
6
|
+
return typeof value === "string" && FULL_GIT_OBJECT_ID_RE.test(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function gitObjectIdWidth(format: GitObjectFormat): 40 | 64 {
|
|
10
|
+
return format === "sha1" ? 40 : 64;
|
|
11
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-read formatting helpers (#162 人读格式助手).
|
|
3
|
+
*
|
|
4
|
+
* Presentation layer only: machine consumers bite full-precision data-* values;
|
|
5
|
+
* these helpers format the same facts for human scanning (中文时长 / 紧凑 token /
|
|
6
|
+
* 成本有效精度 / 本地时刻). Shared by the S1 trajectory page and the S2 board —
|
|
7
|
+
* no structural change to either.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 中文时长: two largest non-zero units (天/小时/分/秒). Non-finite/negative → 0 秒. */
|
|
11
|
+
export function formatDurationZh(ms: number): string {
|
|
12
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0 秒";
|
|
13
|
+
const units: Array<[number, string]> = [
|
|
14
|
+
[86_400_000, "天"],
|
|
15
|
+
[3_600_000, "小时"],
|
|
16
|
+
[60_000, "分"],
|
|
17
|
+
[1000, "秒"],
|
|
18
|
+
];
|
|
19
|
+
let rest = Math.floor(ms);
|
|
20
|
+
const parts: string[] = [];
|
|
21
|
+
for (const [unitMs, label] of units) {
|
|
22
|
+
if (parts.length === 2) break;
|
|
23
|
+
const value = Math.floor(rest / unitMs);
|
|
24
|
+
if (value > 0) {
|
|
25
|
+
parts.push(`${value} ${label}`);
|
|
26
|
+
rest -= value * unitMs;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return parts.length > 0 ? parts.join(" ") : "0 秒";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 紧凑 token: exact below 1000; one-decimal k/M above (rounded). */
|
|
33
|
+
export function formatTokensCompact(tokens: number): string {
|
|
34
|
+
if (!Number.isFinite(tokens) || tokens <= 0) return "0";
|
|
35
|
+
if (tokens < 1000) return String(Math.round(tokens));
|
|
36
|
+
const round1 = (x: number): string => {
|
|
37
|
+
const r = Math.round(x * 10) / 10;
|
|
38
|
+
return Number.isInteger(r) ? String(r) : r.toFixed(1);
|
|
39
|
+
};
|
|
40
|
+
if (tokens < 1_000_000) return `${round1(tokens / 1000)}k`;
|
|
41
|
+
return `${round1(tokens / 1_000_000)}M`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 成本有效精度: ≥100 integer, ≥1 two decimals, <1 four significant digits; zeros stripped. */
|
|
45
|
+
export function formatUsdPrecise(value: number): string {
|
|
46
|
+
if (!Number.isFinite(value) || value === 0) return "0";
|
|
47
|
+
const abs = Math.abs(value);
|
|
48
|
+
const strip = (text: string): string =>
|
|
49
|
+
text.includes(".") ? text.replace(/0+$/, "").replace(/\.$/, "") : text;
|
|
50
|
+
if (abs >= 100) return String(Math.round(value));
|
|
51
|
+
if (abs >= 1) return strip(value.toFixed(2));
|
|
52
|
+
return String(Number(value.toPrecision(4)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 本地时刻: local wall clock of the instant; unparseable input passes through. */
|
|
56
|
+
export function formatLocalDateTime(iso: string): string {
|
|
57
|
+
const ms = Date.parse(iso);
|
|
58
|
+
if (!Number.isFinite(ms)) return iso;
|
|
59
|
+
const d = new Date(ms);
|
|
60
|
+
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
61
|
+
return (
|
|
62
|
+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
63
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin shared in-process AgentSession open (#233).
|
|
3
|
+
* Static Pi imports are safe for Navigator (same module instance as ModelRuntime).
|
|
4
|
+
* Auditor/evidence callers must dynamic-import this module so public-cli bundling
|
|
5
|
+
* does not statically reach @earendil-works/pi-coding-agent.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
createAgentSession,
|
|
9
|
+
DefaultResourceLoader,
|
|
10
|
+
SettingsManager,
|
|
11
|
+
type ModelRuntime,
|
|
12
|
+
type SessionManager,
|
|
13
|
+
type ToolDefinition,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
16
|
+
|
|
17
|
+
export type OpenInProcessAgentSessionOptions = {
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
/** Required when loading a resource loader / systemPrompt. Callers own scratch lifecycle. */
|
|
20
|
+
readonly agentDir?: string;
|
|
21
|
+
readonly model: Model<Api>;
|
|
22
|
+
readonly modelRuntime: ModelRuntime;
|
|
23
|
+
readonly thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
24
|
+
readonly sessionManager: SessionManager;
|
|
25
|
+
readonly systemPrompt?: string;
|
|
26
|
+
readonly customTools?: ToolDefinition[];
|
|
27
|
+
readonly noTools?: "all" | "builtin";
|
|
28
|
+
/** Optional allowlist — omit for unrestricted Pi defaults (ADR 0064). */
|
|
29
|
+
readonly tools?: string[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Single createAgentSession + Settings construction for all in-process children.
|
|
34
|
+
*/
|
|
35
|
+
export async function openInProcessAgentSession(
|
|
36
|
+
options: OpenInProcessAgentSessionOptions,
|
|
37
|
+
): Promise<{ session: Awaited<ReturnType<typeof createAgentSession>>["session"]; dispose(): void }> {
|
|
38
|
+
const settings = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: false } });
|
|
39
|
+
|
|
40
|
+
const createArgs: Parameters<typeof createAgentSession>[0] = {
|
|
41
|
+
cwd: options.cwd,
|
|
42
|
+
model: options.model,
|
|
43
|
+
thinkingLevel: options.thinkingLevel ?? "off",
|
|
44
|
+
modelRuntime: options.modelRuntime,
|
|
45
|
+
sessionManager: options.sessionManager,
|
|
46
|
+
settingsManager: settings,
|
|
47
|
+
...(options.noTools === undefined ? {} : { noTools: options.noTools }),
|
|
48
|
+
...(options.tools === undefined ? {} : { tools: options.tools }),
|
|
49
|
+
...(options.customTools === undefined ? {} : { customTools: options.customTools }),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (options.agentDir !== undefined) {
|
|
53
|
+
const loader = new DefaultResourceLoader({
|
|
54
|
+
cwd: options.cwd,
|
|
55
|
+
agentDir: options.agentDir,
|
|
56
|
+
settingsManager: settings,
|
|
57
|
+
noExtensions: true,
|
|
58
|
+
noSkills: true,
|
|
59
|
+
noPromptTemplates: true,
|
|
60
|
+
noThemes: true,
|
|
61
|
+
noContextFiles: true,
|
|
62
|
+
...(options.systemPrompt === undefined ? {} : { systemPrompt: options.systemPrompt }),
|
|
63
|
+
});
|
|
64
|
+
await loader.reload();
|
|
65
|
+
createArgs.agentDir = options.agentDir;
|
|
66
|
+
createArgs.resourceLoader = loader;
|
|
67
|
+
} else if (options.systemPrompt !== undefined) {
|
|
68
|
+
throw new Error("openInProcessAgentSession requires agentDir when systemPrompt is set");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { session } = await createAgentSession(createArgs);
|
|
72
|
+
return {
|
|
73
|
+
session,
|
|
74
|
+
dispose() {
|
|
75
|
+
session.dispose();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createComplianceDecisionTool,
|
|
5
|
+
runComplianceAudit,
|
|
6
|
+
type ComplianceCompletion,
|
|
7
|
+
type ComplianceDecision,
|
|
8
|
+
} from "./compliance-transport.ts";
|
|
9
|
+
import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
|
|
10
|
+
import { loadAuditorSoul } from "./auditor-soul.ts";
|
|
11
|
+
import {
|
|
12
|
+
readJudgeAuditSubjects,
|
|
13
|
+
resolveAuditDossier,
|
|
14
|
+
toAuditIncomplete,
|
|
15
|
+
} from "./dossier-resolution.ts";
|
|
16
|
+
|
|
17
|
+
export const JUDGE_AUDIT_TOOL_NAME = "ak_soul_audit_decision";
|
|
18
|
+
export const SOUL_AUDIT_TOOL_NAME = JUDGE_AUDIT_TOOL_NAME;
|
|
19
|
+
|
|
20
|
+
export type JudgeAuditOptions = {
|
|
21
|
+
context: ExtensionContext;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const auditDecisionTool = createComplianceDecisionTool(
|
|
26
|
+
JUDGE_AUDIT_TOOL_NAME,
|
|
27
|
+
"Return whether the proposed verdict demonstrably follows the judge soul and dossier evidence.",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Judge auditor: zero hand-delivered materials. Subjects recovered from the
|
|
32
|
+
* parent-session books. Public CLI injects AK_ROLE_RUN_DIR; bare Pi (ADR 0052)
|
|
33
|
+
* proceeds without that pointer and self-locates per soul.
|
|
34
|
+
*/
|
|
35
|
+
export function createPiJudgeAuditor(
|
|
36
|
+
runCompletion?: ComplianceCompletion,
|
|
37
|
+
): (options: JudgeAuditOptions) => Promise<ComplianceDecision> {
|
|
38
|
+
return async (options) => {
|
|
39
|
+
const dossier = resolveAuditDossier();
|
|
40
|
+
if (dossier.status === "incomplete") return toAuditIncomplete(dossier.observation);
|
|
41
|
+
const subjects = readJudgeAuditSubjects(options.context);
|
|
42
|
+
if (subjects.status === "incomplete") return toAuditIncomplete(subjects.observation);
|
|
43
|
+
|
|
44
|
+
return runComplianceAudit({
|
|
45
|
+
tool: auditDecisionTool,
|
|
46
|
+
systemPrompt: await loadAuditorSoul("judge"),
|
|
47
|
+
roleLabel: "Soul compliance audit",
|
|
48
|
+
invalidDecisionLabel: "invalid soul audit decision",
|
|
49
|
+
...(runCompletion === undefined ? {} : { runCompletion }),
|
|
50
|
+
context: options.context,
|
|
51
|
+
...(auditorRunDirectory(options.context) === undefined ? {} : { runDirectory: auditorRunDirectory(options.context) }),
|
|
52
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { sha256Hex } from "./sha256.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* On-demand anti-forge for judge recordings.
|
|
5
|
+
*
|
|
6
|
+
* Standing CI must not byte-pin soul free text. Call these only when a
|
|
7
|
+
* concrete recording is being consumed as evidence and must be checked
|
|
8
|
+
* against the soul body supplied at that moment (current or packaged).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function soulTextDigest(soulText: string): string {
|
|
12
|
+
return sha256Hex(soulText);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertRecordingSoulDigest(options: {
|
|
16
|
+
claimedDigest: unknown;
|
|
17
|
+
soulText: string;
|
|
18
|
+
label: string;
|
|
19
|
+
}): void {
|
|
20
|
+
if (typeof options.claimedDigest !== "string") {
|
|
21
|
+
throw new Error(`${options.label} metadata soulDigest must be a string`);
|
|
22
|
+
}
|
|
23
|
+
const actual = soulTextDigest(options.soulText);
|
|
24
|
+
if (actual !== options.claimedDigest) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`${options.label} metadata soulDigest must match the soul supplied at consumption`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Construct-at-consumption check: resolve the packaged soul via the injected
|
|
33
|
+
* reader, then verify meta.soulDigest. Reader is injected so callers control
|
|
34
|
+
* git/fs access and tests stay synthetic.
|
|
35
|
+
*/
|
|
36
|
+
export function assertPackagedSoulMatchesMeta(
|
|
37
|
+
meta: Record<string, unknown>,
|
|
38
|
+
label: string,
|
|
39
|
+
readPackagedSoul: (packageSha: string, label: string) => string,
|
|
40
|
+
): void {
|
|
41
|
+
if (typeof meta.packageSha !== "string") {
|
|
42
|
+
throw new Error(`${label} metadata packageSha must be a string`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof meta.soulDigest !== "string") {
|
|
45
|
+
throw new Error(`${label} metadata soulDigest must be a string`);
|
|
46
|
+
}
|
|
47
|
+
const packagedSoul = readPackagedSoul(meta.packageSha, label);
|
|
48
|
+
assertRecordingSoulDigest({
|
|
49
|
+
claimedDigest: meta.soulDigest,
|
|
50
|
+
soulText: packagedSoul,
|
|
51
|
+
label,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
AgentToolResult,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Type, type Static } from "typebox";
|
|
8
|
+
|
|
9
|
+
import { disposeComplianceDecision } from "./audit-escalation.ts";
|
|
10
|
+
import type { ComplianceDecision } from "./compliance-transport.ts";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
JUDGE_OUTPUT_TOOL_NAME,
|
|
14
|
+
validateAcceptedJudgeDetails,
|
|
15
|
+
type JudgeVerdict,
|
|
16
|
+
} from "./package-contracts/judge-output.ts";
|
|
17
|
+
|
|
18
|
+
export { JUDGE_OUTPUT_TOOL_NAME };
|
|
19
|
+
export type { JudgeVerdict };
|
|
20
|
+
|
|
21
|
+
const judgeVerdictSchema = Type.Object(
|
|
22
|
+
{
|
|
23
|
+
judgeStatus: StringEnum(["converged", "continue", "escalate"] as const, { description: "Judge adjudication outcome discriminator." }),
|
|
24
|
+
fix: Type.Optional(
|
|
25
|
+
Type.Object(
|
|
26
|
+
{ summary: Type.String({ minLength: 1, description: "Required remediation summary." }) },
|
|
27
|
+
{ additionalProperties: false, description: "Remediation requested when adjudication must continue." },
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
classes: Type.Optional(Type.Array(Type.Object({
|
|
31
|
+
name: Type.String({ minLength: 1 }),
|
|
32
|
+
owner: Type.String({ minLength: 1 }),
|
|
33
|
+
boundary: Type.String({ minLength: 1 }),
|
|
34
|
+
disposition: Type.String({ minLength: 1 }),
|
|
35
|
+
}, { additionalProperties: false }), { minItems: 1, description: "Adjudicated finding classes with owner and repair boundary." })),
|
|
36
|
+
note: Type.Optional(Type.String({ minLength: 1, description: "Optional adjudication note." })),
|
|
37
|
+
evidence: Type.Optional(Type.Unknown({ description: "Retained adjudication evidence." })),
|
|
38
|
+
decisionGate: Type.Optional(
|
|
39
|
+
Type.Object(
|
|
40
|
+
{
|
|
41
|
+
question: Type.String({ minLength: 1 }),
|
|
42
|
+
options: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
43
|
+
},
|
|
44
|
+
{ additionalProperties: false, description: "Question and options requiring human authority." },
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
{ additionalProperties: true },
|
|
49
|
+
);
|
|
50
|
+
(judgeVerdictSchema as unknown as { required: string[] }).required = [];
|
|
51
|
+
|
|
52
|
+
type JudgeVerdictParameters = Static<typeof judgeVerdictSchema>;
|
|
53
|
+
|
|
54
|
+
export type SoulAuditResult = ComplianceDecision;
|
|
55
|
+
|
|
56
|
+
export type JudgeRoleDependencies = {
|
|
57
|
+
loadSoul(): Promise<string>;
|
|
58
|
+
auditSoulCompliance(
|
|
59
|
+
options: { context: ExtensionContext; signal?: AbortSignal },
|
|
60
|
+
): Promise<SoulAuditResult>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type JudgeRoleHostActions = {
|
|
64
|
+
failInfrastructure(error: unknown, ctx: ExtensionContext, toolCallId?: string): never;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function validateVerdict(verdict: JudgeVerdictParameters): JudgeVerdict {
|
|
68
|
+
return validateAcceptedJudgeDetails(verdict);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function requireSingletonSubmissionCall(
|
|
72
|
+
toolCallId: string,
|
|
73
|
+
ctx: ExtensionContext,
|
|
74
|
+
): void {
|
|
75
|
+
const leaf = ctx.sessionManager.getLeafEntry();
|
|
76
|
+
if (leaf?.type !== "message" || leaf.message.role !== "assistant") {
|
|
77
|
+
throw new Error("Judge output must be the sole final tool call");
|
|
78
|
+
}
|
|
79
|
+
const calls = leaf.message.content.filter((part) => part.type === "toolCall");
|
|
80
|
+
const call = calls[0];
|
|
81
|
+
if (
|
|
82
|
+
calls.length !== 1 || call === undefined || call.id !== toolCallId ||
|
|
83
|
+
call.name !== JUDGE_OUTPUT_TOOL_NAME
|
|
84
|
+
) {
|
|
85
|
+
throw new Error("Judge output must be the sole final tool call");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createJudgeRoleRuntime(
|
|
90
|
+
pi: ExtensionAPI,
|
|
91
|
+
dependencies: JudgeRoleDependencies,
|
|
92
|
+
hostActions: JudgeRoleHostActions,
|
|
93
|
+
): { activate(): Promise<void> } {
|
|
94
|
+
let soul: string | undefined;
|
|
95
|
+
let lifecycleRegistered = false;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
async activate() {
|
|
99
|
+
soul = (await dependencies.loadSoul()).trim();
|
|
100
|
+
if (soul.length === 0) throw new Error("Judge soul is empty");
|
|
101
|
+
if (!lifecycleRegistered) {
|
|
102
|
+
lifecycleRegistered = true;
|
|
103
|
+
pi.registerTool({
|
|
104
|
+
name: JUDGE_OUTPUT_TOOL_NAME,
|
|
105
|
+
label: "Judge Output",
|
|
106
|
+
description:
|
|
107
|
+
"Submit the final judge verdict. Soul compliance is audited before acceptance.",
|
|
108
|
+
promptSnippet: "Submit the final judge verdict after adjudication",
|
|
109
|
+
promptGuidelines: [
|
|
110
|
+
`Use ${JUDGE_OUTPUT_TOOL_NAME} as the final action for the judge role.`,
|
|
111
|
+
],
|
|
112
|
+
parameters: judgeVerdictSchema,
|
|
113
|
+
async execute(toolCallId, parameters, signal, _onUpdate, ctx): Promise<AgentToolResult<unknown>> {
|
|
114
|
+
if (soul === undefined) throw new Error("Judge soul was not loaded");
|
|
115
|
+
requireSingletonSubmissionCall(toolCallId, ctx);
|
|
116
|
+
const verdict = validateVerdict(parameters);
|
|
117
|
+
// Candidate verdict is already on the parent session books as this
|
|
118
|
+
// tool-call leaf (first-record-then-audit; run 019fea05 L61/L62).
|
|
119
|
+
let audit: SoulAuditResult;
|
|
120
|
+
try {
|
|
121
|
+
audit = await dependencies.auditSoulCompliance(
|
|
122
|
+
signal === undefined
|
|
123
|
+
? { context: ctx }
|
|
124
|
+
: { context: ctx, signal },
|
|
125
|
+
);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
hostActions.failInfrastructure(error, ctx, toolCallId);
|
|
128
|
+
}
|
|
129
|
+
return disposeComplianceDecision<AgentToolResult<unknown>>(
|
|
130
|
+
audit,
|
|
131
|
+
{
|
|
132
|
+
pass: (usage) => ({
|
|
133
|
+
content: [{ type: "text" as const, text: "Judge verdict accepted" }],
|
|
134
|
+
details: verdict,
|
|
135
|
+
terminate: true as const,
|
|
136
|
+
...(usage === undefined ? {} : { usage }),
|
|
137
|
+
}),
|
|
138
|
+
revise: (violations) => {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Judge verdict violates its soul: ${violations.join("; ")}`,
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
escalate: (result) => result,
|
|
144
|
+
auditIncomplete: (result) => result,
|
|
145
|
+
},
|
|
146
|
+
verdict,
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
pi.on("before_agent_start", (event) => {
|
|
151
|
+
if (soul === undefined) throw new Error("Judge soul was not loaded");
|
|
152
|
+
return {
|
|
153
|
+
systemPrompt:
|
|
154
|
+
`${event.systemPrompt}\n\n<judge_soul>\n${soul}\n</judge_soul>`,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
import { isFullGitObjectId } from "./git-object-id.ts";
|
|
3
|
+
import { exactUtf8 } from "./exact-utf8.ts";
|
|
4
|
+
import { sha256Hex } from "./sha256.ts";
|
|
5
|
+
import { openToolObjectFromUnion } from "./open-tool-schema.ts";
|
|
6
|
+
|
|
7
|
+
const oidPattern = "^(?:[0-9a-f]{40}|[0-9a-f]{64})$";
|
|
8
|
+
const materialSchema = Type.Object({ bytesBase64: Type.String(), sha256: Type.String() }, { additionalProperties: false });
|
|
9
|
+
const checkSchema = Type.Object({ name: Type.String({ minLength: 1 }), argv: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }) }, { additionalProperties: false });
|
|
10
|
+
export const mergerInputSchema = Type.Object({
|
|
11
|
+
version: Type.Literal(1), attemptId: Type.String({ minLength: 1 }),
|
|
12
|
+
targetObjectId: Type.String({ pattern: oidPattern }), sourceObjectId: Type.String({ pattern: oidPattern }),
|
|
13
|
+
materials: Type.Object({ task: materialSchema, authority: materialSchema, targetIntent: materialSchema, sourceIntent: materialSchema }, { additionalProperties: false }),
|
|
14
|
+
expectedConflictPaths: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
15
|
+
resolutionScope: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
16
|
+
authorizedChecks: Type.Array(checkSchema),
|
|
17
|
+
}, { additionalProperties: false });
|
|
18
|
+
const mergerOutputVariants = Type.Union([
|
|
19
|
+
Type.Object({ status: Type.Literal("completed", { description: "Merge attempt completed." }), attemptId: Type.String({ minLength: 1, description: "Identity of the admitted merge attempt." }), report: Type.String({ minLength: 1, description: "Truthful merge outcome report." }), mergeCommitId: Type.String({ pattern: oidPattern, description: "Verified completed merge commit object ID." }) }, { additionalProperties: false }),
|
|
20
|
+
Type.Object({ status: Type.Literal("escalate", { description: "Merge attempt requires human authority." }), attemptId: Type.String({ minLength: 1, description: "Identity of the admitted merge attempt." }), diagnosis: Type.String({ minLength: 1, description: "Reason merge completion requires escalation." }), report: Type.String({ minLength: 1, description: "Truthful merge outcome report." }) }, { additionalProperties: false }),
|
|
21
|
+
]);
|
|
22
|
+
export const mergerOutputSchema = openToolObjectFromUnion(mergerOutputVariants);
|
|
23
|
+
|
|
24
|
+
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;
|
|
25
|
+
export type MergerMaterial = DeepReadonly<Static<typeof materialSchema>>;
|
|
26
|
+
export type MergerInput = DeepReadonly<Static<typeof mergerInputSchema>>;
|
|
27
|
+
export type MergerOutput =
|
|
28
|
+
| { status: "completed"; attemptId: string; report: string; mergeCommitId: string }
|
|
29
|
+
| { status: "escalate"; attemptId: string; diagnosis: string; report: string };
|
|
30
|
+
export const MERGER_OUTPUT_TOOL_NAME = "ak_merger_output";
|
|
31
|
+
export const MERGER_ACCEPTED_TEXT = "Merger output accepted";
|
|
32
|
+
|
|
33
|
+
const record = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34
|
+
const blank = (v: unknown) => typeof v !== "string" || v.trim().length === 0;
|
|
35
|
+
export class MergerInputContractError extends Error {
|
|
36
|
+
constructor(message = "Merger input violates its exact contract") { super(message); this.name = "MergerInputContractError"; }
|
|
37
|
+
}
|
|
38
|
+
function fail(message = "Merger input violates its exact contract"): never { throw new MergerInputContractError(message); }
|
|
39
|
+
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 !== ".."); }
|
|
40
|
+
function validatePathSet(value: unknown, label: string): string[] {
|
|
41
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every(canonicalPath)) fail(`Merger ${label} must be a non-empty canonical path set`);
|
|
42
|
+
return value as string[];
|
|
43
|
+
}
|
|
44
|
+
function validateMaterial(value: unknown, label: string): void {
|
|
45
|
+
if (!record(value) || typeof value.bytesBase64 !== "string" || typeof value.sha256 !== "string") fail(`Merger ${label} material is malformed`);
|
|
46
|
+
const bytes = Buffer.from(value.bytesBase64, "base64");
|
|
47
|
+
exactUtf8(bytes, `Merger ${label} material`);
|
|
48
|
+
if (sha256Hex(bytes) !== value.sha256) fail(`Merger ${label} material digest mismatch`);
|
|
49
|
+
}
|
|
50
|
+
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; }
|
|
51
|
+
|
|
52
|
+
export function validateMergerInput(value: unknown): MergerInput {
|
|
53
|
+
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");
|
|
54
|
+
if (!record(value.materials)) fail();
|
|
55
|
+
for (const key of ["task", "authority", "targetIntent", "sourceIntent"] as const) validateMaterial(value.materials[key], key);
|
|
56
|
+
const conflicts = validatePathSet(value.expectedConflictPaths, "expected conflict paths");
|
|
57
|
+
const scope = validatePathSet(value.resolutionScope, "resolution scope");
|
|
58
|
+
if (!conflicts.every(path => scope.includes(path))) fail("Merger resolution scope must contain the complete conflict set");
|
|
59
|
+
if (!Array.isArray(value.authorizedChecks)) fail("Merger authorized checks are malformed");
|
|
60
|
+
for (const check of value.authorizedChecks) {
|
|
61
|
+
if (!record(check) || !Array.isArray(check.argv) || check.argv.length === 0 || check.argv.some(blank)) fail("Merger authorized check is malformed");
|
|
62
|
+
}
|
|
63
|
+
return deepFreeze(structuredClone(value) as MergerInput);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateMergerOutput(value: unknown, expectedAttemptId?: string): MergerOutput {
|
|
67
|
+
if (!record(value) || (expectedAttemptId !== undefined && value.attemptId !== expectedAttemptId)) throw new Error("Merger output attempt mismatch");
|
|
68
|
+
if (value.status === "completed" && isFullGitObjectId(value.mergeCommitId)) return structuredClone(value) as MergerOutput;
|
|
69
|
+
if (value.status === "escalate") return structuredClone(value) as MergerOutput;
|
|
70
|
+
throw new Error("Merger output has no recognized execution discriminator");
|
|
71
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { exactUtf8 } from "./exact-utf8.ts";
|
|
4
|
+
import { isFullGitObjectId } from "./git-object-id.ts";
|
|
5
|
+
|
|
6
|
+
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[] };
|
|
9
|
+
export interface MergerGitState {
|
|
10
|
+
activeMerge(): Promise<ActiveMergerGitState>;
|
|
11
|
+
completedMerge(mergeCommitId: string, automaticMergeTreeId: string): Promise<CompletedMergerGitState>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function git(cwd: string, args: string[]): Promise<Uint8Array> {
|
|
15
|
+
const { stdout } = await execFileAsync("git", args, { cwd, encoding: "buffer", maxBuffer: 16 * 1024 * 1024 });
|
|
16
|
+
return new Uint8Array(stdout);
|
|
17
|
+
}
|
|
18
|
+
function line(bytes: Uint8Array, label: string): string {
|
|
19
|
+
const value = exactUtf8(bytes, label).trim();
|
|
20
|
+
if (!value) throw new Error(`${label} is empty`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
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)));
|
|
27
|
+
}
|
|
28
|
+
async function unmerged(cwd: string): Promise<string[]> {
|
|
29
|
+
const raw = exactUtf8(await git(cwd, ["ls-files", "-u", "-z"]), "Git unmerged index");
|
|
30
|
+
const paths = new Set<string>();
|
|
31
|
+
for (const row of raw.split("\0")) {
|
|
32
|
+
if (!row) continue;
|
|
33
|
+
const tab = row.indexOf("\t");
|
|
34
|
+
if (tab < 0 || tab === row.length - 1) throw new Error("Git returned a malformed unmerged index row");
|
|
35
|
+
paths.add(row.slice(tab + 1));
|
|
36
|
+
}
|
|
37
|
+
return [...paths].sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createProductionMergerGitState(repositoryRoot = process.cwd()): MergerGitState {
|
|
41
|
+
return {
|
|
42
|
+
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");
|
|
49
|
+
}
|
|
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 };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
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";
|
|
4
|
+
import { exactUtf8 } from "./exact-utf8.ts";
|
|
5
|
+
import { isFullGitObjectId } from "./git-object-id.ts";
|
|
6
|
+
|
|
7
|
+
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 const MERGER_ACTIVE_TOOLS = ["read", "grep", "find", "ls", "bash", "write", "edit", MERGER_OUTPUT_TOOL_NAME] as const;
|
|
10
|
+
export type MergerRoleDependencies = { loadSoul(): Promise<string>; loadInput(path: string): Promise<unknown>; gitState: MergerGitState };
|
|
11
|
+
export type MergerRoleHostActions = { failInfrastructure(error: unknown, ctx: ExtensionContext, toolCallId?: string): never };
|
|
12
|
+
|
|
13
|
+
function same(a: readonly string[], b: readonly string[]): boolean { return a.length === b.length && a.every((value, i) => value === b[i]); }
|
|
14
|
+
function singleton(id: string, ctx: ExtensionContext): void { const leaf = ctx.sessionManager.getLeafEntry(); if (leaf?.type !== "message" || leaf.message.role !== "assistant") throw new Error("Merger output must be the sole final tool call"); const calls = leaf.message.content.filter(part => part.type === "toolCall"); if (calls.length !== 1 || calls[0]?.id !== id || calls[0]?.name !== MERGER_OUTPUT_TOOL_NAME) throw new Error("Merger output must be the sole final tool call"); }
|
|
15
|
+
function materialText(input: MergerInput, key: keyof MergerInput["materials"]): string { return exactUtf8(Buffer.from(input.materials[key].bytesBase64, "base64"), `Merger ${key}`); }
|
|
16
|
+
|
|
17
|
+
export function createMergerRoleRuntime(pi: ExtensionAPI, dependencies: MergerRoleDependencies, host: MergerRoleHostActions) {
|
|
18
|
+
let activation: { soul: string; input: Readonly<MergerInput>; automaticMergeTreeId: string } | undefined;
|
|
19
|
+
let registered = false;
|
|
20
|
+
let accepted = false;
|
|
21
|
+
pi.registerFlag(MERGER_INPUT_FLAG.name, MERGER_INPUT_FLAG.definition);
|
|
22
|
+
return { async activate() {
|
|
23
|
+
const path = pi.getFlag(MERGER_INPUT_FLAG.name);
|
|
24
|
+
if (typeof path !== "string" || path.trim().length === 0) throw new Error("Merger requires --ak-merger-input");
|
|
25
|
+
const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Merger soul is empty");
|
|
26
|
+
const input = validateMergerInput(await dependencies.loadInput(path));
|
|
27
|
+
const state = await dependencies.gitState.activeMerge();
|
|
28
|
+
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");
|
|
29
|
+
activation = { soul, input, automaticMergeTreeId: state.automaticMergeTreeId }; accepted = false;
|
|
30
|
+
if (!registered) { registered = true;
|
|
31
|
+
pi.registerTool({ name: MERGER_OUTPUT_TOOL_NAME, label: "Merger Output", description: "Submit the sole completed merge candidate or genuine intent/authority escalation.", promptSnippet: "Submit the Merger result", promptGuidelines: [`Use ${MERGER_OUTPUT_TOOL_NAME} as the sole final action.`, "Infrastructure and Git failures abort; they are not escalations."], parameters: mergerOutputSchema,
|
|
32
|
+
async execute(id, params, _signal, _update, ctx) {
|
|
33
|
+
if (!activation) throw new Error("Merger is not activated");
|
|
34
|
+
if (accepted) throw new Error("Merger output already accepted");
|
|
35
|
+
let output;
|
|
36
|
+
try {
|
|
37
|
+
singleton(id, ctx);
|
|
38
|
+
output = validateMergerOutput(params, activation.input.attemptId);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
host.failInfrastructure(error, ctx, id);
|
|
41
|
+
}
|
|
42
|
+
if (output.status === "completed") {
|
|
43
|
+
let state;
|
|
44
|
+
try { state = await dependencies.gitState.completedMerge(output.mergeCommitId, activation.automaticMergeTreeId); }
|
|
45
|
+
catch (error) { host.failInfrastructure(error, ctx, id); }
|
|
46
|
+
const scope = new Set(activation.input.resolutionScope);
|
|
47
|
+
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);
|
|
48
|
+
}
|
|
49
|
+
accepted = true;
|
|
50
|
+
return { content: [{ type: "text" as const, text: MERGER_ACCEPTED_TEXT }], details: output, terminate: true as const };
|
|
51
|
+
} });
|
|
52
|
+
pi.on("before_agent_start", event => { if (!activation) throw new Error("Merger is not activated"); 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>` }; });
|
|
53
|
+
}
|
|
54
|
+
const all = pi.getAllTools().map(tool => tool.name);
|
|
55
|
+
for (const name of MERGER_ACTIVE_TOOLS) if (all.filter(item => item === name).length !== 1) throw new Error(`Merger required tool collision or missing: ${name}`);
|
|
56
|
+
pi.setActiveTools([...MERGER_ACTIVE_TOOLS]);
|
|
57
|
+
const active = pi.getActiveTools?.() ?? [...MERGER_ACTIVE_TOOLS];
|
|
58
|
+
if (!same(active, MERGER_ACTIVE_TOOLS)) throw new Error("Merger active tool narrowing failed");
|
|
59
|
+
} };
|
|
60
|
+
}
|