@akagilnc/pi-workflow-roles 0.1.3758 → 0.1.3783
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/README.zh-CN.md +3 -2
- package/dist/acp-host/production-host.js +1735 -948
- package/dist/collector-config.js +0 -1
- package/dist/collector-github.js +199 -2
- package/dist/collector-identity.js +128 -41
- package/dist/collector-ledger.js +38 -9
- package/dist/collector-receipt.js +19 -7
- package/dist/collector-role.js +330 -369
- package/dist/collector-target.js +169 -0
- package/dist/collector-tool-schemas.js +51 -14
- package/dist/headless-host/description.js +77 -0
- package/dist/headless-host/mcp-relay.mjs +119 -0
- package/dist/headless-host/production-host.js +26590 -0
- package/dist/host-descriptions.js +44 -3
- package/dist/package-contracts/collector-output.js +32 -0
- package/dist/package-contracts/terminating-infrastructure.js +13 -12
- package/dist/pi/role-turn-host.js +1 -2
- package/dist/public-cli/github-remote.js +45 -0
- package/dist/public-cli/invocation.js +53 -54
- package/dist/public-cli/load-production-external-host.js +19 -0
- package/dist/public-cli/load-production-headless-host.js +36 -0
- package/dist/public-cli/main.js +857 -310
- package/dist/public-cli/option-definitions.js +6 -4
- package/dist/public-cli/run-lifecycle.js +3 -3
- package/dist/public-cli/settlement.js +83 -6
- package/dist/public-role-summons.js +40 -5
- package/dist/role-runtime.js +137 -7
- package/dist/submission-correctable-error.js +24 -0
- package/extensions/role-runtime.ts +0 -1
- package/package.json +1 -1
- package/scripts/build-package.mjs +28 -0
- package/src/acp-host/role-envelope.ts +146 -103
- package/src/acp-host/role-turn-host.ts +12 -0
- package/src/collector-config.ts +0 -1
- package/src/collector-github.ts +236 -2
- package/src/collector-identity.ts +148 -40
- package/src/collector-ledger.ts +48 -10
- package/src/collector-receipt.ts +33 -14
- package/src/collector-role.ts +376 -450
- package/src/collector-target.ts +207 -0
- package/src/collector-tool-schemas.ts +62 -15
- package/src/headless-host/description.ts +123 -0
- package/src/headless-host/production-host.ts +75 -0
- package/src/headless-host/role-turn-host.ts +424 -0
- package/src/host-contracts.ts +2 -1
- package/src/host-descriptions.ts +53 -6
- package/src/package-contracts/collector-output.ts +72 -0
- package/src/package-contracts/terminating-infrastructure.ts +24 -13
- package/src/pi/role-turn-host.ts +1 -2
- package/src/public-cli/cli.ts +12 -3
- package/src/public-cli/collector-run.ts +3 -2
- package/src/public-cli/github-remote.ts +45 -0
- package/src/public-cli/invocation.ts +60 -59
- package/src/public-cli/load-production-external-host.ts +29 -0
- package/src/public-cli/load-production-headless-host.ts +48 -0
- package/src/public-cli/main.ts +5 -0
- package/src/public-cli/option-definitions.ts +6 -4
- package/src/public-cli/run-lifecycle.ts +2 -2
- package/src/public-cli/settlement.ts +82 -6
- package/src/public-role-summons.ts +62 -9
- package/src/role-runtime.ts +166 -13
- package/src/submission-correctable-error.ts +38 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #676 D1: Collector admission target recognition on structured git context.
|
|
3
|
+
* Explicit PR wins (no association queries). Otherwise gather online candidates
|
|
4
|
+
* from structured upstream head and HEAD commit association only. Deduped unique
|
|
5
|
+
* → bind; many → require explicit --pr; zero → unbound so the role can decide
|
|
6
|
+
* from task materials via the bind-target business tool. No task-text scrape,
|
|
7
|
+
* no second state machine. Git/config/transport failures keep true cause.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
import type { CollectorRepository } from "./collector-config.ts";
|
|
12
|
+
import {
|
|
13
|
+
createGhApiRunner,
|
|
14
|
+
listPullRequestNumbersByCommit,
|
|
15
|
+
listPullRequestNumbersByHead,
|
|
16
|
+
} from "./collector-github.ts";
|
|
17
|
+
import { CliUsageError } from "./public-cli/cli-errors.ts";
|
|
18
|
+
import {
|
|
19
|
+
ownerFromGitHubRemoteUrl,
|
|
20
|
+
ownerRepoFromGitHubRemoteUrl,
|
|
21
|
+
} from "./public-cli/github-remote.ts";
|
|
22
|
+
|
|
23
|
+
export type ResolveCollectorTargetInput = {
|
|
24
|
+
readonly projectRoot: string;
|
|
25
|
+
readonly repository: CollectorRepository;
|
|
26
|
+
/** Caller-provided PR number when already explicit. */
|
|
27
|
+
readonly explicitPrNumber?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CollectorTargetResolution =
|
|
31
|
+
| { readonly kind: "bound"; readonly prNumber: number }
|
|
32
|
+
| { readonly kind: "unbound" };
|
|
33
|
+
|
|
34
|
+
function ambiguousTarget(detail: string, cause?: unknown): never {
|
|
35
|
+
throw new CliUsageError(
|
|
36
|
+
`collector target is ambiguous: ${detail}; pass an explicit --pr`,
|
|
37
|
+
cause === undefined ? undefined : { cause },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Real git failure — not target ambiguity. Propagates with true cause on exit 1. */
|
|
42
|
+
function gitFailure(detail: string, cause: unknown): never {
|
|
43
|
+
throw new Error(`collector git failed: ${detail}`, {
|
|
44
|
+
cause: cause instanceof Error ? cause : new Error(String(cause)),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function gitText(projectRoot: string, args: readonly string[]): string {
|
|
49
|
+
return execFileSync("git", [...args], {
|
|
50
|
+
cwd: projectRoot,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
53
|
+
}).trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** git config --get exit 1 = key absent; other failures keep true cause. */
|
|
57
|
+
function isGitConfigMissing(error: unknown): boolean {
|
|
58
|
+
if (typeof error !== "object" || error === null) return false;
|
|
59
|
+
const status = (error as { status?: unknown }).status;
|
|
60
|
+
return status === 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readCurrentBranch(projectRoot: string): string {
|
|
64
|
+
try {
|
|
65
|
+
return gitText(projectRoot, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
gitFailure("cannot read current git branch", error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readHeadSha(projectRoot: string): string {
|
|
72
|
+
try {
|
|
73
|
+
return gitText(projectRoot, ["rev-parse", "HEAD"]);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
gitFailure("cannot read current HEAD", error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Head ref short name from branch.<name>.merge (refs/heads/<ref> or bare ref).
|
|
81
|
+
* Non-heads merge forms are not a structured head binding.
|
|
82
|
+
*/
|
|
83
|
+
function headRefFromMerge(merge: string): string | undefined {
|
|
84
|
+
const trimmed = merge.trim();
|
|
85
|
+
if (trimmed.length === 0) return undefined;
|
|
86
|
+
if (trimmed.startsWith("refs/heads/")) {
|
|
87
|
+
const ref = trimmed.slice("refs/heads/".length);
|
|
88
|
+
return ref.length > 0 ? ref : undefined;
|
|
89
|
+
}
|
|
90
|
+
// Reject other refs/* (e.g. refs/remotes/…) — not a PR head ref binding.
|
|
91
|
+
if (trimmed.startsWith("refs/")) return undefined;
|
|
92
|
+
return trimmed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Real upstream head owner + head ref from branch.<name>.remote/merge when configured.
|
|
97
|
+
* Missing config keys = not configured (undefined). Config/remote execution failures
|
|
98
|
+
* throw with true cause — never swallowed as "no upstream, keep fallback".
|
|
99
|
+
*/
|
|
100
|
+
function readUpstreamHeadBinding(
|
|
101
|
+
projectRoot: string,
|
|
102
|
+
branch: string,
|
|
103
|
+
): { readonly headOwner: string; readonly headRef: string } | undefined {
|
|
104
|
+
let remote: string | undefined;
|
|
105
|
+
try {
|
|
106
|
+
remote = gitText(projectRoot, ["config", "--get", `branch.${branch}.remote`]);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (isGitConfigMissing(error)) remote = undefined;
|
|
109
|
+
else gitFailure(`cannot read branch.${branch}.remote`, error);
|
|
110
|
+
}
|
|
111
|
+
if (remote === undefined || remote.length === 0) return undefined;
|
|
112
|
+
|
|
113
|
+
let merge: string | undefined;
|
|
114
|
+
try {
|
|
115
|
+
merge = gitText(projectRoot, ["config", "--get", `branch.${branch}.merge`]);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (isGitConfigMissing(error)) merge = undefined;
|
|
118
|
+
else gitFailure(`cannot read branch.${branch}.merge`, error);
|
|
119
|
+
}
|
|
120
|
+
if (merge === undefined || merge.length === 0) return undefined;
|
|
121
|
+
|
|
122
|
+
const headRef = headRefFromMerge(merge);
|
|
123
|
+
if (headRef === undefined) return undefined;
|
|
124
|
+
|
|
125
|
+
let remoteUrl: string;
|
|
126
|
+
try {
|
|
127
|
+
remoteUrl = gitText(projectRoot, ["remote", "get-url", remote]);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
gitFailure(`cannot read remote URL for ${remote}`, error);
|
|
130
|
+
}
|
|
131
|
+
const headOwner = ownerFromGitHubRemoteUrl(remoteUrl);
|
|
132
|
+
if (headOwner === undefined) return undefined;
|
|
133
|
+
return { headOwner, headRef };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the Collector PR target without guessing from task text.
|
|
138
|
+
* - Explicit `--pr` → that number (no association queries).
|
|
139
|
+
* - Otherwise: full online candidates (upstream head + HEAD commit), deduped.
|
|
140
|
+
* - Exactly 1 → bound. >1 → require explicit `--pr`. 0 → unbound (role bind tool).
|
|
141
|
+
* - Git/config/transport/HTTP/JSON failures propagate with true cause.
|
|
142
|
+
*/
|
|
143
|
+
export async function resolveCollectorTarget(
|
|
144
|
+
input: ResolveCollectorTargetInput,
|
|
145
|
+
): Promise<CollectorTargetResolution> {
|
|
146
|
+
if (input.explicitPrNumber !== undefined) {
|
|
147
|
+
return { kind: "bound", prNumber: input.explicitPrNumber };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const branch = readCurrentBranch(input.projectRoot);
|
|
151
|
+
const detached = branch.length === 0 || branch === "HEAD";
|
|
152
|
+
|
|
153
|
+
// Detached HEAD cannot associate via branch head; commit association still runs.
|
|
154
|
+
const runner = createGhApiRunner();
|
|
155
|
+
const { owner, repo } = input.repository;
|
|
156
|
+
const numbers: number[] = [];
|
|
157
|
+
|
|
158
|
+
if (!detached) {
|
|
159
|
+
const headSha = readHeadSha(input.projectRoot);
|
|
160
|
+
const upstream = readUpstreamHeadBinding(input.projectRoot, branch);
|
|
161
|
+
|
|
162
|
+
// Prefer structured head owner:ref from real upstream merge binding (fork-safe).
|
|
163
|
+
if (upstream !== undefined) {
|
|
164
|
+
numbers.push(
|
|
165
|
+
...(await listPullRequestNumbersByHead(runner, {
|
|
166
|
+
owner,
|
|
167
|
+
repo,
|
|
168
|
+
headOwner: upstream.headOwner,
|
|
169
|
+
headRef: upstream.headRef,
|
|
170
|
+
})),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
// Always also take commit association — never let a sole upstream hit hide a conflict.
|
|
174
|
+
numbers.push(
|
|
175
|
+
...(await listPullRequestNumbersByCommit(runner, {
|
|
176
|
+
owner,
|
|
177
|
+
repo,
|
|
178
|
+
commitSha: headSha,
|
|
179
|
+
})),
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
// Detached: commit association only (no branch head).
|
|
183
|
+
const headSha = readHeadSha(input.projectRoot);
|
|
184
|
+
numbers.push(
|
|
185
|
+
...(await listPullRequestNumbersByCommit(runner, {
|
|
186
|
+
owner,
|
|
187
|
+
repo,
|
|
188
|
+
commitSha: headSha,
|
|
189
|
+
})),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const unique = [...new Set(numbers)];
|
|
194
|
+
|
|
195
|
+
if (unique.length === 1) {
|
|
196
|
+
return { kind: "bound", prNumber: unique[0]! };
|
|
197
|
+
}
|
|
198
|
+
if (unique.length > 1) {
|
|
199
|
+
ambiguousTarget(
|
|
200
|
+
`multiple PRs associated with context: ${unique.join(", ")}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
// Zero structured git hits — leave unbound so the role can decide from materials.
|
|
204
|
+
return { kind: "unbound" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { ownerRepoFromGitHubRemoteUrl, ownerFromGitHubRemoteUrl };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
2
|
import { COLLECTOR_ELIGIBILITY_MS } from "./collector-evidence.ts";
|
|
3
|
+
import { openToolObject } from "./open-tool-schema.ts";
|
|
3
4
|
import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
|
|
4
5
|
|
|
5
6
|
export const collectorObserveArgsSchema = Type.Object({}, { additionalProperties: false });
|
|
@@ -15,31 +16,77 @@ export const collectorWaitArgsSchema = Type.Object({
|
|
|
15
16
|
}, { additionalProperties: false });
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
* #
|
|
19
|
-
*
|
|
20
|
-
* the
|
|
21
|
-
* (repo/PR/comment id/url/author/kind/时间) and validates resolvability.
|
|
22
|
-
* Category is a short LLM classification label, never a body transcription.
|
|
19
|
+
* #676 A: role-decided target bind. The model judges task materials and submits
|
|
20
|
+
* the chosen PR and/or issue identity; runtime only performs online association
|
|
21
|
+
* for the role-chosen ticket — never scrapes task text to lock a target.
|
|
23
22
|
*/
|
|
24
|
-
export const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}, { additionalProperties: false });
|
|
28
|
-
|
|
29
|
-
export const collectorOutputBaseSchema = Type.Object({
|
|
30
|
-
findings: Type.Optional(Type.Array(collectorFindingArgsSchema, {
|
|
31
|
-
description: "本次收集到的逐条 findings;零 finding 的模板通知不得进入。正常完工无 finding 时省略。",
|
|
23
|
+
export const collectorBindTargetArgsSchema = Type.Object({
|
|
24
|
+
prNumber: Type.Optional(Type.Unknown({
|
|
25
|
+
description: "角色判定的本仓 PR 号(正整数)。与 issueNumber 二选一或同指唯一目标。形状指引,非 schema 闸。",
|
|
32
26
|
})),
|
|
33
|
-
|
|
27
|
+
issueNumber: Type.Optional(Type.Unknown({
|
|
28
|
+
description: "角色判定的本仓 issue 号(正整数);runtime 经线上关联解析唯一 PR。形状指引,非 schema 闸。",
|
|
29
|
+
})),
|
|
30
|
+
}, { additionalProperties: true });
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* #641 chain① / #676 C / ADR 0057: nested finding item declarations for model
|
|
34
|
+
* guidance. Host must not pure-shape-reject the envelope (第 0 条). Runtime binds
|
|
35
|
+
* resolvable evidence pointers only; unprojected content stays distinguishable.
|
|
36
|
+
*/
|
|
37
|
+
const collectorFindingItemDeclaration = (() => {
|
|
38
|
+
// Nested declarations for model guidance only — open required so host cannot
|
|
39
|
+
// pure-shape-reject missing optional fields (第 0 条 / ADR 0057 / #676 C).
|
|
40
|
+
const item = Type.Object(
|
|
41
|
+
{
|
|
42
|
+
evidenceId: Type.Unknown({
|
|
43
|
+
description: "observe 返回的材料指针(必填语义)",
|
|
44
|
+
}),
|
|
45
|
+
category: Type.Unknown({
|
|
46
|
+
description: "简短归类标签,不是摘要",
|
|
47
|
+
}),
|
|
48
|
+
summary: Type.Unknown({
|
|
49
|
+
description: "哪个 bot、什么问题的摘要;不誊抄正文",
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
additionalProperties: true,
|
|
54
|
+
description:
|
|
55
|
+
"单条 finding 指针:evidenceId + 可选 category/summary。形状指引,非 schema 闸。",
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
(item as unknown as { required: string[] }).required = [];
|
|
59
|
+
return item;
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Field declarations + descriptions are guidance for the model — host must not
|
|
64
|
+
* pure-shape-reject the envelope (第 0 条 / ADR 0057).
|
|
65
|
+
*/
|
|
66
|
+
export const collectorOutputBaseSchema = openToolObject(
|
|
67
|
+
Type.Object({
|
|
68
|
+
// No root type:array — host must not shape-reject non-array findings (#676 C).
|
|
69
|
+
// Nested item declarations ride `items` for registration preservation (ADR 0057).
|
|
70
|
+
findings: Type.Unsafe({
|
|
71
|
+
description:
|
|
72
|
+
"本次收集到的逐条 findings(指针数组为规范形)。零 finding 的模板通知不得进入;正常完工无 finding 时省略。形状指引,非 schema 闸。",
|
|
73
|
+
items: collectorFindingItemDeclaration,
|
|
74
|
+
}),
|
|
75
|
+
unfinishedReasons: Type.Unknown({
|
|
76
|
+
description:
|
|
77
|
+
"未完成原因字符串数组(额度/故障/等待届满等现场依据);不得把未完成表述为无问题。无可报告时省略。形状指引,非 schema 闸。",
|
|
78
|
+
}),
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
34
81
|
|
|
35
82
|
/** Runtime owns the observed evidence; the model submits findings and signals sole-final submission. */
|
|
36
83
|
export const collectorOutputArgsSchema = withInfrastructureFailureDeclaration(
|
|
37
84
|
collectorOutputBaseSchema,
|
|
38
85
|
);
|
|
39
|
-
(collectorOutputArgsSchema as unknown as { required: string[] }).required = [];
|
|
40
86
|
|
|
41
87
|
export type CollectorObserveArgs = Static<typeof collectorObserveArgsSchema>;
|
|
42
88
|
export type CollectorRequestArgs = Static<typeof collectorRequestArgsSchema>;
|
|
43
89
|
export type CollectorReadArgs = Static<typeof collectorReadArgsSchema>;
|
|
44
90
|
export type CollectorWaitArgs = Static<typeof collectorWaitArgsSchema>;
|
|
91
|
+
export type CollectorBindTargetArgs = Static<typeof collectorBindTargetArgsSchema>;
|
|
45
92
|
export type CollectorOutputArgs = Static<typeof collectorOutputArgsSchema>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One headless CLI host description (#645 / #752).
|
|
3
|
+
* Every host-specific value the generic headless adapter needs — binary, argv
|
|
4
|
+
* shape, session binding — is data here; lifecycle stays one copy so #646 codex
|
|
5
|
+
* is another row, not a fork.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export type HeadlessHostDescription = Readonly<{
|
|
10
|
+
/** Binary path segments relative to the operator home. */
|
|
11
|
+
binaryFromHome: readonly string[];
|
|
12
|
+
/** Durable session-id binding filename beside the session principal. */
|
|
13
|
+
sessionBindingFile: string;
|
|
14
|
+
/**
|
|
15
|
+
* Host-native print-mode flags that never change per turn (no prompt).
|
|
16
|
+
* Model / effort / system-prompt / schema / session / resume / mcp-config
|
|
17
|
+
* are composed by the adapter from the turn request — not listed here.
|
|
18
|
+
*/
|
|
19
|
+
fixedArgs: readonly string[];
|
|
20
|
+
/** Print-mode flag that takes the user prompt as its value (e.g. `-p`). */
|
|
21
|
+
promptFlag: string;
|
|
22
|
+
/** CLI flag for the seat model (e.g. `--model`). */
|
|
23
|
+
modelFlag: string;
|
|
24
|
+
/** CLI flag for the seat thinking level (e.g. `--effort`); value is opaque pass-through. */
|
|
25
|
+
effortFlag: string;
|
|
26
|
+
/**
|
|
27
|
+
* CLI flag whose value is a path to the system-prompt file
|
|
28
|
+
* (`--system-prompt-file`). File delivery keeps ARG_MAX off the critical path.
|
|
29
|
+
*/
|
|
30
|
+
systemPromptFlag: string;
|
|
31
|
+
/** CLI flag whose value is a JSON Schema document string. */
|
|
32
|
+
jsonSchemaFlag: string;
|
|
33
|
+
/** CLI flag whose value is a path or JSON string for MCP servers. */
|
|
34
|
+
mcpConfigFlag: string;
|
|
35
|
+
/** CLI flag to mint a fresh session id (initial turn). */
|
|
36
|
+
sessionIdFlag: string;
|
|
37
|
+
/** CLI flag to resume a prior session id. */
|
|
38
|
+
resumeFlag: string;
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
/** Absolute agent binary for one operator home. */
|
|
42
|
+
export function resolveHeadlessBinary(
|
|
43
|
+
description: HeadlessHostDescription,
|
|
44
|
+
operatorHome: string,
|
|
45
|
+
): string {
|
|
46
|
+
return join(operatorHome, ...description.binaryFromHome);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build one headless CLI argv for a single process turn.
|
|
51
|
+
* Shape: `<promptFlag> <prompt> <fixedArgs…> <system/schema/mcp/model/effort/session…>`.
|
|
52
|
+
*/
|
|
53
|
+
export function headlessTurnArgs(options: {
|
|
54
|
+
readonly description: HeadlessHostDescription;
|
|
55
|
+
readonly prompt: string;
|
|
56
|
+
/** Absolute path written by the adapter; paired with `systemPromptFlag`. */
|
|
57
|
+
readonly systemPromptPath: string;
|
|
58
|
+
readonly jsonSchema: Readonly<Record<string, unknown>>;
|
|
59
|
+
/** Absolute path to host-native MCP config JSON; omitted when no AK MCP servers. */
|
|
60
|
+
readonly mcpConfigPath?: string;
|
|
61
|
+
readonly model?: string;
|
|
62
|
+
readonly effort?: string;
|
|
63
|
+
/** Fresh session: pass as session id. Resume: pass as resume id. */
|
|
64
|
+
readonly session: { readonly kind: "new"; readonly id: string } | { readonly kind: "resume"; readonly id: string };
|
|
65
|
+
}): string[] {
|
|
66
|
+
const { description } = options;
|
|
67
|
+
const args: string[] = [
|
|
68
|
+
description.promptFlag,
|
|
69
|
+
options.prompt,
|
|
70
|
+
...description.fixedArgs,
|
|
71
|
+
description.systemPromptFlag,
|
|
72
|
+
options.systemPromptPath,
|
|
73
|
+
description.jsonSchemaFlag,
|
|
74
|
+
JSON.stringify(options.jsonSchema),
|
|
75
|
+
];
|
|
76
|
+
if (options.mcpConfigPath !== undefined && options.mcpConfigPath !== "") {
|
|
77
|
+
args.push(description.mcpConfigFlag, options.mcpConfigPath);
|
|
78
|
+
}
|
|
79
|
+
if (options.model !== undefined && options.model !== "") {
|
|
80
|
+
args.push(description.modelFlag, options.model);
|
|
81
|
+
}
|
|
82
|
+
if (options.effort !== undefined && options.effort !== "") {
|
|
83
|
+
args.push(description.effortFlag, options.effort);
|
|
84
|
+
}
|
|
85
|
+
if (options.session.kind === "new") {
|
|
86
|
+
args.push(description.sessionIdFlag, options.session.id);
|
|
87
|
+
} else {
|
|
88
|
+
args.push(description.resumeFlag, options.session.id);
|
|
89
|
+
}
|
|
90
|
+
return args;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Project shared-envelope MCP server rows into Claude `--mcp-config` JSON.
|
|
95
|
+
* Env stays a plain object (Claude CLI shape); ACP rows use `{name,value}[]`.
|
|
96
|
+
*/
|
|
97
|
+
export function headlessMcpConfigDocument(
|
|
98
|
+
mcpServers: readonly Readonly<Record<string, unknown>>[],
|
|
99
|
+
): Readonly<{ mcpServers: Readonly<Record<string, Readonly<Record<string, unknown>>>> }> {
|
|
100
|
+
const servers: Record<string, Record<string, unknown>> = {};
|
|
101
|
+
for (const row of mcpServers) {
|
|
102
|
+
const name = typeof row.name === "string" ? row.name : undefined;
|
|
103
|
+
const command = typeof row.command === "string" ? row.command : undefined;
|
|
104
|
+
if (name === undefined || name === "" || command === undefined || command === "") continue;
|
|
105
|
+
const entry: Record<string, unknown> = { command };
|
|
106
|
+
if (Array.isArray(row.args)) entry.args = row.args;
|
|
107
|
+
if (Array.isArray(row.env)) {
|
|
108
|
+
const env: Record<string, string> = {};
|
|
109
|
+
for (const item of row.env) {
|
|
110
|
+
if (typeof item !== "object" || item === null) continue;
|
|
111
|
+
const record = item as { name?: unknown; value?: unknown };
|
|
112
|
+
if (typeof record.name === "string" && typeof record.value === "string") {
|
|
113
|
+
env[record.name] = record.value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(env).length > 0) entry.env = env;
|
|
117
|
+
} else if (typeof row.env === "object" && row.env !== null && !Array.isArray(row.env)) {
|
|
118
|
+
entry.env = row.env;
|
|
119
|
+
}
|
|
120
|
+
servers[name] = entry;
|
|
121
|
+
}
|
|
122
|
+
return Object.freeze({ mcpServers: Object.freeze(servers) });
|
|
123
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production composition for the generic headless CLI RoleTurnHost (#645).
|
|
3
|
+
* Agent subprocesses inherit the operator home and credentials in place.
|
|
4
|
+
* No HOME rewrite, no isolated home, no credential parameters — CLI owns auth.
|
|
5
|
+
* Sitian records on the run are the dossier; host private sessions stay private.
|
|
6
|
+
*
|
|
7
|
+
* Intermediate AK tools ride the shared envelope MCP relay via host-native
|
|
8
|
+
* `--mcp-config` under `--strict-mcp-config`. The terminating receipt is the
|
|
9
|
+
* host-native `--json-schema` / structured_output schema channel only
|
|
10
|
+
* (#750 submission-tool-is-schema-channel) — terminating tool is not listed on MCP.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
|
|
15
|
+
import { createAcpRoleRuntimeDependencies } from "../acp-host/production-host.ts";
|
|
16
|
+
import { prepareAcpRoleEnvelope } from "../acp-host/role-envelope.ts";
|
|
17
|
+
import { createAcpSessionIdentityAuthority } from "../acp-host/session-identity.ts";
|
|
18
|
+
import { resolveHeadlessBinary, type HeadlessHostDescription } from "./description.ts";
|
|
19
|
+
import { createHeadlessRoleTurnHost } from "./role-turn-host.ts";
|
|
20
|
+
|
|
21
|
+
export type ProductionHeadlessHostOptions = Readonly<{
|
|
22
|
+
packageRoot: string;
|
|
23
|
+
principalAuthority: DurablePrincipalAuthority;
|
|
24
|
+
description: HeadlessHostDescription;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Assemble a production headless RoleTurnHost from the shared envelope prepare
|
|
29
|
+
* (MCP relay for AK tools) and one host description row. Binary is resolved
|
|
30
|
+
* from each turn's operator home (`request.home`).
|
|
31
|
+
*/
|
|
32
|
+
export function createProductionHeadlessRoleTurnHost(
|
|
33
|
+
options: ProductionHeadlessHostOptions,
|
|
34
|
+
): RoleTurnHost {
|
|
35
|
+
const { packageRoot, principalAuthority, description } = options;
|
|
36
|
+
const sessionIdentity = createAcpSessionIdentityAuthority(
|
|
37
|
+
principalAuthority,
|
|
38
|
+
description.sessionBindingFile,
|
|
39
|
+
);
|
|
40
|
+
const roleRuntimeDependencies = createAcpRoleRuntimeDependencies(packageRoot);
|
|
41
|
+
|
|
42
|
+
const innerFor = (operatorHome: string): RoleTurnHost =>
|
|
43
|
+
createHeadlessRoleTurnHost({
|
|
44
|
+
description,
|
|
45
|
+
sessionIdentity,
|
|
46
|
+
binary: resolveHeadlessBinary(description, operatorHome),
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
AK_PACKAGE_ROOT: packageRoot,
|
|
50
|
+
},
|
|
51
|
+
prepare: (request) =>
|
|
52
|
+
prepareAcpRoleEnvelope({
|
|
53
|
+
request,
|
|
54
|
+
dependencies: roleRuntimeDependencies,
|
|
55
|
+
sessionFile: sessionIdentity.resolveSessionFile(request.principal),
|
|
56
|
+
// Same MCP relay as ACP so intermediate AK tools stay reachable;
|
|
57
|
+
// headless adapter projects the row into --mcp-config.
|
|
58
|
+
socketPath: `/tmp/ak-headless-mcp-${randomUUID()}.sock`,
|
|
59
|
+
// Schema channel owns the terminating receipt; hide it from MCP list.
|
|
60
|
+
listTerminatingToolOnMcp: false,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let cachedHome: string | undefined;
|
|
65
|
+
let cachedHost: RoleTurnHost | undefined;
|
|
66
|
+
return {
|
|
67
|
+
executeTurn(request) {
|
|
68
|
+
if (cachedHost === undefined || cachedHome !== request.home) {
|
|
69
|
+
cachedHome = request.home;
|
|
70
|
+
cachedHost = innerFor(request.home);
|
|
71
|
+
}
|
|
72
|
+
return cachedHost.executeTurn(request);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|