@akagilnc/pi-workflow-roles 0.1.3999 → 0.1.4021
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/dist/acp-host/production-host.js +10 -0
- package/dist/headless-host/description.js +319 -21
- package/dist/headless-host/production-host.js +462 -52
- package/dist/host-descriptions.js +19 -6
- package/dist/public-cli/main.js +10 -0
- package/package.json +1 -1
- package/src/headless-host/description.ts +381 -20
- package/src/headless-host/production-host.ts +5 -5
- package/src/headless-host/role-turn-host.ts +290 -27
- package/src/host-descriptions.ts +20 -7
|
@@ -2702,6 +2702,7 @@ var init_host_descriptions = __esm({
|
|
|
2702
2702
|
});
|
|
2703
2703
|
HEADLESS_HOST_DESCRIPTIONS = Object.freeze({
|
|
2704
2704
|
"claude": Object.freeze({
|
|
2705
|
+
protocol: "claude-print",
|
|
2705
2706
|
binaryFromHome: Object.freeze([".local", "bin", "claude"]),
|
|
2706
2707
|
sessionBindingFile: "claude-headless-session.json",
|
|
2707
2708
|
fixedArgs: Object.freeze([
|
|
@@ -2727,6 +2728,15 @@ var init_host_descriptions = __esm({
|
|
|
2727
2728
|
mcpConfigFlag: "--mcp-config",
|
|
2728
2729
|
sessionIdFlag: "--session-id",
|
|
2729
2730
|
resumeFlag: "--resume"
|
|
2731
|
+
}),
|
|
2732
|
+
/**
|
|
2733
|
+
* Codex headless (#646). Binary under operator home; auth stays in CODEX_HOME.
|
|
2734
|
+
* Argv/parse/schema-close are codex-exec helpers — not Claude flag mapping.
|
|
2735
|
+
*/
|
|
2736
|
+
"codex": Object.freeze({
|
|
2737
|
+
protocol: "codex-exec",
|
|
2738
|
+
binaryFromHome: Object.freeze([".local", "bin", "codex"]),
|
|
2739
|
+
sessionBindingFile: "codex-headless-session.json"
|
|
2730
2740
|
})
|
|
2731
2741
|
});
|
|
2732
2742
|
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Headless CLI host descriptions (#645 / #646 / #752).
|
|
3
|
+
* Shared lifecycle owns spawn/bind/close; each protocol owns argv + parse shape.
|
|
4
|
+
* Claude print-mode and codex exec differ enough that #752 host-specific
|
|
5
|
+
* assembly lives here as sibling helpers — not a third unified abstraction.
|
|
6
6
|
*/
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
export function isClaudePrintDescription(description) {
|
|
9
|
+
return description.protocol === "claude-print";
|
|
10
|
+
}
|
|
11
|
+
export function isCodexExecDescription(description) {
|
|
12
|
+
return description.protocol === "codex-exec";
|
|
13
|
+
}
|
|
8
14
|
/** Absolute agent binary for one operator home. */
|
|
9
15
|
export function resolveHeadlessBinary(description, operatorHome) {
|
|
10
16
|
return join(operatorHome, ...description.binaryFromHome);
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
13
|
-
* Build one
|
|
19
|
+
* Build one Claude print-mode argv for a single process turn.
|
|
14
20
|
* Shape: `<promptFlag> <prompt> <fixedArgs…> <system/schema/mcp/model/effort/session…>`.
|
|
15
21
|
*/
|
|
16
22
|
export function headlessTurnArgs(options) {
|
|
@@ -41,6 +47,311 @@ export function headlessTurnArgs(options) {
|
|
|
41
47
|
}
|
|
42
48
|
return args;
|
|
43
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* TOML string literal for `codex -c key=<value>` (values are TOML-parsed).
|
|
52
|
+
* Double-quoted form; escapes backslash and quote only.
|
|
53
|
+
*/
|
|
54
|
+
function codexTomlString(value) {
|
|
55
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
56
|
+
}
|
|
57
|
+
/** TOML array of strings for `-c key=["a","b"]`. */
|
|
58
|
+
function codexTomlStringArray(values) {
|
|
59
|
+
return `[${values.map(codexTomlString).join(",")}]`;
|
|
60
|
+
}
|
|
61
|
+
/** TOML inline table of string→string for `-c key={a="b"}`. */
|
|
62
|
+
function codexTomlStringTable(entries) {
|
|
63
|
+
const parts = Object.entries(entries).map(([key, value]) => `${key}=${codexTomlString(value)}`);
|
|
64
|
+
return `{${parts.join(",")}}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Free-form JSON leaf for Type.Unknown under Codex strict transport.
|
|
68
|
+
* Strict rejects bare untyped nodes and root-level additionalProperties:true;
|
|
69
|
+
* a $defs anyOf of JSON values (with nested additionalProperties as $ref)
|
|
70
|
+
* is accepted and keeps array/object receipts expressible (navigator candidates).
|
|
71
|
+
* Package code still does not validate or reject the receipt against schema.
|
|
72
|
+
*/
|
|
73
|
+
const CODEX_JSON_VALUE_DEF = "codexJsonValue";
|
|
74
|
+
const CODEX_JSON_VALUE_REF = `#/$defs/${CODEX_JSON_VALUE_DEF}`;
|
|
75
|
+
const CODEX_JSON_VALUE_SCHEMA = Object.freeze({
|
|
76
|
+
anyOf: Object.freeze([
|
|
77
|
+
Object.freeze({ type: "string" }),
|
|
78
|
+
Object.freeze({ type: "number" }),
|
|
79
|
+
Object.freeze({ type: "boolean" }),
|
|
80
|
+
Object.freeze({ type: "null" }),
|
|
81
|
+
Object.freeze({ type: "array", items: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }) }),
|
|
82
|
+
Object.freeze({
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: Object.freeze({}),
|
|
85
|
+
required: Object.freeze([]),
|
|
86
|
+
additionalProperties: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }),
|
|
87
|
+
}),
|
|
88
|
+
]),
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* Derive a Codex/OpenAI-strict transport schema from the package open schema.
|
|
92
|
+
* Legal open schema is untouched; this is a host-only transmission projection
|
|
93
|
+
* (#646 / 0057 法意 / 0054 strict): every object closes, every property is
|
|
94
|
+
* required, and only originally-optional fields become a null union (official
|
|
95
|
+
* guidance: emulate optional via type|null). Originally-required fields stay
|
|
96
|
+
* non-nullable. Nested open-tool anyOf wrappers are flattened so every branch
|
|
97
|
+
* carries `type`. Type.Unknown / description-only leaves become a free JSON
|
|
98
|
+
* $ref. Package code still does not validate or reject the receipt.
|
|
99
|
+
*/
|
|
100
|
+
export function closeJsonSchemaForCodex(schema) {
|
|
101
|
+
const closed = closeSchemaNode(schema);
|
|
102
|
+
const existingDefs = isPlainObject(closed.$defs)
|
|
103
|
+
? closed.$defs
|
|
104
|
+
: {};
|
|
105
|
+
return {
|
|
106
|
+
...closed,
|
|
107
|
+
$defs: {
|
|
108
|
+
...existingDefs,
|
|
109
|
+
[CODEX_JSON_VALUE_DEF]: CODEX_JSON_VALUE_SCHEMA,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Shared plain-object guard for headless host schema/event reduction. */
|
|
114
|
+
export function isPlainObject(value) {
|
|
115
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
/** Pure null leaf only — composite type|null is stripped in-leaf, not dropped whole. */
|
|
118
|
+
function isPureNullTypeSchema(value) {
|
|
119
|
+
if (!isPlainObject(value))
|
|
120
|
+
return false;
|
|
121
|
+
if (value.type === "null")
|
|
122
|
+
return true;
|
|
123
|
+
if (Array.isArray(value.type) && value.type.length > 0 && value.type.every((t) => t === "null")) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Within a leaf, remove null members from type arrays.
|
|
130
|
+
* Required edges keep the non-null type(s); optional edges re-add null once.
|
|
131
|
+
*/
|
|
132
|
+
function stripNullFromLeafType(leaf) {
|
|
133
|
+
if (!isPlainObject(leaf) || !Array.isArray(leaf.type))
|
|
134
|
+
return leaf;
|
|
135
|
+
const nonNull = leaf.type.filter((t) => t !== "null");
|
|
136
|
+
if (nonNull.length === leaf.type.length)
|
|
137
|
+
return leaf;
|
|
138
|
+
if (nonNull.length === 0)
|
|
139
|
+
return { ...leaf, type: "null" };
|
|
140
|
+
if (nonNull.length === 1)
|
|
141
|
+
return { ...leaf, type: nonNull[0] };
|
|
142
|
+
return { ...leaf, type: nonNull };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Flatten nested anyOf wrappers into concrete leaf schemas.
|
|
146
|
+
* Open-tool unions often wrap leaves in description-only anyOf shells that
|
|
147
|
+
* lack `type`; strict structured output rejects those intermediate nodes.
|
|
148
|
+
*/
|
|
149
|
+
function flattenUnionLeaves(schema) {
|
|
150
|
+
if (!isPlainObject(schema))
|
|
151
|
+
return [schema];
|
|
152
|
+
if (Array.isArray(schema.anyOf)) {
|
|
153
|
+
return schema.anyOf.flatMap(flattenUnionLeaves);
|
|
154
|
+
}
|
|
155
|
+
return [schema];
|
|
156
|
+
}
|
|
157
|
+
/** Drop pure-null leaves after in-leaf null stripping; optional edges re-add null once. */
|
|
158
|
+
function nonNullLeaves(schema) {
|
|
159
|
+
return flattenUnionLeaves(schema)
|
|
160
|
+
.map(stripNullFromLeafType)
|
|
161
|
+
.filter((leaf) => !isPureNullTypeSchema(leaf));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Property edge under strict transport.
|
|
165
|
+
* Required → closed non-null leaf(s). Optional → closed leaf(s) + null.
|
|
166
|
+
*/
|
|
167
|
+
function closePropertySchema(schema, optional) {
|
|
168
|
+
const leaves = nonNullLeaves(schema).map(closeSchemaNode);
|
|
169
|
+
if (leaves.length === 0)
|
|
170
|
+
return { type: "null" };
|
|
171
|
+
if (!optional) {
|
|
172
|
+
return leaves.length === 1 ? leaves[0] : { anyOf: leaves };
|
|
173
|
+
}
|
|
174
|
+
return { anyOf: [...leaves, { type: "null" }] };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Strict generators require every schema node to declare `type` (or a $ref).
|
|
178
|
+
* Type.Unknown / description-only leaves → free JSON $ref (not string): array
|
|
179
|
+
* and object receipts stay expressible under --output-schema.
|
|
180
|
+
*/
|
|
181
|
+
function ensureTypedLeaf(schema) {
|
|
182
|
+
if (schema.type !== undefined)
|
|
183
|
+
return schema;
|
|
184
|
+
if (typeof schema.$ref === "string")
|
|
185
|
+
return schema;
|
|
186
|
+
if (isPlainObject(schema.properties) || schema.additionalProperties !== undefined) {
|
|
187
|
+
return { ...schema, type: "object" };
|
|
188
|
+
}
|
|
189
|
+
if (schema.items !== undefined) {
|
|
190
|
+
return { ...schema, type: "array" };
|
|
191
|
+
}
|
|
192
|
+
if (schema.const !== undefined) {
|
|
193
|
+
const value = schema.const;
|
|
194
|
+
if (value === null)
|
|
195
|
+
return { ...schema, type: "null" };
|
|
196
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
197
|
+
return { ...schema, type: typeof value };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
201
|
+
const sample = schema.enum.find((item) => item !== null);
|
|
202
|
+
if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
|
|
203
|
+
return { ...schema, type: typeof sample };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Free JSON via $defs. $ref must stand alone (strict rejects sibling keys).
|
|
207
|
+
return { $ref: CODEX_JSON_VALUE_REF };
|
|
208
|
+
}
|
|
209
|
+
function originalRequiredNames(node) {
|
|
210
|
+
if (!Array.isArray(node.required))
|
|
211
|
+
return new Set();
|
|
212
|
+
return new Set(node.required.filter((item) => typeof item === "string"));
|
|
213
|
+
}
|
|
214
|
+
function closeSchemaNode(node) {
|
|
215
|
+
if (!isPlainObject(node))
|
|
216
|
+
return node;
|
|
217
|
+
// Already a ref (free-JSON leaf or pre-existing) — do not retype.
|
|
218
|
+
if (typeof node.$ref === "string")
|
|
219
|
+
return node;
|
|
220
|
+
// Union node: flatten then close each concrete leaf (do not keep untyped shells).
|
|
221
|
+
if (Array.isArray(node.anyOf)) {
|
|
222
|
+
const leaves = nonNullLeaves(node).map(closeSchemaNode);
|
|
223
|
+
if (leaves.length === 0)
|
|
224
|
+
return { type: "null" };
|
|
225
|
+
if (leaves.length === 1)
|
|
226
|
+
return leaves[0];
|
|
227
|
+
return { anyOf: leaves };
|
|
228
|
+
}
|
|
229
|
+
let out = { ...node };
|
|
230
|
+
if (node.items !== undefined) {
|
|
231
|
+
out.items = closeSchemaNode(node.items);
|
|
232
|
+
}
|
|
233
|
+
if (isPlainObject(node.$defs)) {
|
|
234
|
+
out.$defs = Object.fromEntries(Object.entries(node.$defs).map(([key, value]) => [key, closeSchemaNode(value)]));
|
|
235
|
+
}
|
|
236
|
+
const hasProperties = isPlainObject(node.properties);
|
|
237
|
+
const isObjectType = node.type === "object"
|
|
238
|
+
|| (Array.isArray(node.type) && node.type.includes("object"))
|
|
239
|
+
|| hasProperties
|
|
240
|
+
|| node.additionalProperties !== undefined;
|
|
241
|
+
if (hasProperties) {
|
|
242
|
+
const props = node.properties;
|
|
243
|
+
const wasRequired = originalRequiredNames(node);
|
|
244
|
+
const closedProps = {};
|
|
245
|
+
const required = [];
|
|
246
|
+
for (const [name, propSchema] of Object.entries(props)) {
|
|
247
|
+
required.push(name);
|
|
248
|
+
closedProps[name] = closePropertySchema(propSchema, !wasRequired.has(name));
|
|
249
|
+
}
|
|
250
|
+
out.properties = closedProps;
|
|
251
|
+
out.required = required;
|
|
252
|
+
out.additionalProperties = false;
|
|
253
|
+
if (out.type === undefined)
|
|
254
|
+
out.type = "object";
|
|
255
|
+
// Object nodes must not also carry residual anyOf from the open copy.
|
|
256
|
+
delete out.anyOf;
|
|
257
|
+
}
|
|
258
|
+
else if (isObjectType) {
|
|
259
|
+
out.additionalProperties = false;
|
|
260
|
+
if (!Array.isArray(out.required))
|
|
261
|
+
out.required = [];
|
|
262
|
+
if (out.type === undefined)
|
|
263
|
+
out.type = "object";
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
out = ensureTypedLeaf(out);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Project shared-envelope MCP rows into `codex -c mcp_servers.<name>.*` argv pairs.
|
|
272
|
+
* Dot-path + TOML values per official config-advanced; spawn argv (no shell).
|
|
273
|
+
*/
|
|
274
|
+
function stringEnvironment(value) {
|
|
275
|
+
const entries = Array.isArray(value)
|
|
276
|
+
? value.flatMap((item) => {
|
|
277
|
+
if (!isPlainObject(item) || typeof item.name !== "string" || typeof item.value !== "string")
|
|
278
|
+
return [];
|
|
279
|
+
return [[item.name, item.value]];
|
|
280
|
+
})
|
|
281
|
+
: isPlainObject(value)
|
|
282
|
+
? Object.entries(value).filter((entry) => typeof entry[1] === "string")
|
|
283
|
+
: [];
|
|
284
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
285
|
+
}
|
|
286
|
+
function codexMcpConfigArgs(mcpServers) {
|
|
287
|
+
const args = [];
|
|
288
|
+
for (const row of mcpServers) {
|
|
289
|
+
const name = typeof row.name === "string" ? row.name : undefined;
|
|
290
|
+
const command = typeof row.command === "string" ? row.command : undefined;
|
|
291
|
+
if (name === undefined || name === "" || command === undefined || command === "")
|
|
292
|
+
continue;
|
|
293
|
+
const prefix = `mcp_servers.${name}`;
|
|
294
|
+
// required=true: fail the exec if the AK relay cannot handshake (official:
|
|
295
|
+
// required MCP init failure exits with error). Without it, a silent drop
|
|
296
|
+
// would hide a broken intermediate-tool channel (#646 须真跑证②).
|
|
297
|
+
args.push("-c", `${prefix}.command=${codexTomlString(command)}`);
|
|
298
|
+
args.push("-c", `${prefix}.required=true`);
|
|
299
|
+
if (Array.isArray(row.args) && row.args.every((item) => typeof item === "string")) {
|
|
300
|
+
args.push("-c", `${prefix}.args=${codexTomlStringArray(row.args)}`);
|
|
301
|
+
}
|
|
302
|
+
const env = stringEnvironment(row.env);
|
|
303
|
+
if (env !== undefined) {
|
|
304
|
+
args.push("-c", `${prefix}.env=${codexTomlStringTable(env)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return args;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Build one `codex exec` / `codex exec resume` argv (#646).
|
|
311
|
+
* New: `exec --approve-for-me … prompt`.
|
|
312
|
+
* Resume: `exec --approve-for-me resume <thread_id> … prompt`.
|
|
313
|
+
* Approval stays on the parent command so new and resumed turns use Codex's
|
|
314
|
+
* automatic review with its workspace-write sandbox.
|
|
315
|
+
*/
|
|
316
|
+
export function codexTurnArgs(options) {
|
|
317
|
+
// Parent-command flag must precede the resume subcommand.
|
|
318
|
+
const args = ["exec", "--approve-for-me"];
|
|
319
|
+
if (options.session.kind === "resume") {
|
|
320
|
+
args.push("resume", options.session.id);
|
|
321
|
+
}
|
|
322
|
+
// JSONL event stream: thread_id + final agent_message + turn.completed/failed.
|
|
323
|
+
args.push("--json");
|
|
324
|
+
// Operator config/MCP off; auth still uses CODEX_HOME (official).
|
|
325
|
+
// Project/system config and AGENTS.md have no official suppression switch.
|
|
326
|
+
args.push("--ignore-user-config", "--ignore-rules");
|
|
327
|
+
const roots = (options.writableRoots ?? []).filter((root) => root !== "");
|
|
328
|
+
if (roots.length > 0) {
|
|
329
|
+
// Resume has no --add-dir; the config key keeps extra roots available on both paths.
|
|
330
|
+
args.push("-c", `sandbox_workspace_write.writable_roots=${codexTomlStringArray(roots)}`);
|
|
331
|
+
if (options.session.kind === "new") {
|
|
332
|
+
for (const root of roots) {
|
|
333
|
+
args.push("--add-dir", root);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
args.push("-c", `model_instructions_file=${codexTomlString(options.systemPromptPath)}`);
|
|
338
|
+
args.push("--output-schema", options.outputSchemaPath);
|
|
339
|
+
args.push(...codexMcpConfigArgs(options.mcpServers));
|
|
340
|
+
if (options.model !== undefined && options.model !== "") {
|
|
341
|
+
args.push("-m", options.model);
|
|
342
|
+
}
|
|
343
|
+
if (options.effort !== undefined && options.effort !== "") {
|
|
344
|
+
args.push("-c", `model_reasoning_effort=${codexTomlString(options.effort)}`);
|
|
345
|
+
}
|
|
346
|
+
if (options.skipGitRepoCheck === true) {
|
|
347
|
+
args.push("--skip-git-repo-check");
|
|
348
|
+
}
|
|
349
|
+
// Prompt last as positional. `--` stops option parsing so a leading `-`
|
|
350
|
+
// (markdown lists, pasted flags, rulings) is not eaten by clap (codex tip:
|
|
351
|
+
// "use '-- -s'"). Same for new and resume — both end here.
|
|
352
|
+
args.push("--", options.prompt);
|
|
353
|
+
return args;
|
|
354
|
+
}
|
|
44
355
|
/**
|
|
45
356
|
* Project shared-envelope MCP server rows into Claude `--mcp-config` JSON.
|
|
46
357
|
* Env stays a plain object (Claude CLI shape); ACP rows use `{name,value}[]`.
|
|
@@ -55,22 +366,9 @@ export function headlessMcpConfigDocument(mcpServers) {
|
|
|
55
366
|
const entry = { command };
|
|
56
367
|
if (Array.isArray(row.args))
|
|
57
368
|
entry.args = row.args;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (typeof item !== "object" || item === null)
|
|
62
|
-
continue;
|
|
63
|
-
const record = item;
|
|
64
|
-
if (typeof record.name === "string" && typeof record.value === "string") {
|
|
65
|
-
env[record.name] = record.value;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (Object.keys(env).length > 0)
|
|
69
|
-
entry.env = env;
|
|
70
|
-
}
|
|
71
|
-
else if (typeof row.env === "object" && row.env !== null && !Array.isArray(row.env)) {
|
|
72
|
-
entry.env = row.env;
|
|
73
|
-
}
|
|
369
|
+
const env = stringEnvironment(row.env);
|
|
370
|
+
if (env !== undefined)
|
|
371
|
+
entry.env = env;
|
|
74
372
|
servers[name] = entry;
|
|
75
373
|
}
|
|
76
374
|
return Object.freeze({ mcpServers: Object.freeze(servers) });
|