@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.
@@ -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
- * 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.
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 headless CLI argv for a single process turn.
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
- if (Array.isArray(row.env)) {
59
- const env = {};
60
- for (const item of row.env) {
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) });