@akagilnc/pi-workflow-roles 0.1.2086 → 0.1.2091

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 CHANGED
@@ -114,7 +114,7 @@ Generated from `src/public-cli/option-definitions.ts`. Prefer `ak-role help <com
114
114
  | --- | --- | --- | --- | --- | --- | --- | --- |
115
115
  | `--model` | — | `provider/model` | no | no | option | — | Override the effective seat model for this invocation (before or after the command). |
116
116
  | `--thinking` | — | `level` | no | no | option | — | Override thinking level: off\|minimal\|low\|medium\|high\|xhigh\|max. |
117
- | `--engine` | — | `name` | no | no | option | — | Optional Judge labor engine for this invocation (owner pool-directive name; packaged notes attached when present; judge-only). |
117
+ | `--engine` | — | `name` | no | no | option | — | Optional Judge/Reviewer labor engine for this invocation (owner pool-directive name; packaged notes attached when present; judge+reviewer only). |
118
118
  | `--help` | `-h` | — | no | no | option | — | Show public CLI help and exit. |
119
119
 
120
120
  ### `judge`
package/README.zh-CN.md CHANGED
@@ -143,7 +143,7 @@ Codex fast 档:开启:`echo "fast_mode = on" > ~/.pi-codex-fast`;关闭:
143
143
  | --- | --- | --- | --- | --- | --- | --- | --- |
144
144
  | `--model` | — | `provider/model` | 否 | 否 | option | — | 覆盖本调用有效席位模型(可置于子命令前或后)。 |
145
145
  | `--thinking` | — | `level` | 否 | 否 | option | — | 覆盖 thinking 档位:off\|minimal\|low\|medium\|high\|xhigh\|max。 |
146
- | `--engine` | — | `name` | 否 | 否 | option | — | 本调用可选 Judge 劳动引擎(池令名字;有包内调法笔记则附卷;仅 Judge)。 |
146
+ | `--engine` | — | `name` | 否 | 否 | option | — | 本调用可选 Judge/Reviewer 劳动引擎(池令名字;有包内调法笔记则附卷;仅 Judge+Reviewer)。 |
147
147
  | `--help` | `-h` | — | 否 | 否 | option | — | 显示公开 CLI 帮助并退出。 |
148
148
 
149
149
  ### `judge`
@@ -0,0 +1,93 @@
1
+ import { Type } from "typebox";
2
+ import { ENGINE_DETOUR_ALREADY_USED_DIAGNOSTIC, ENGINE_DETOUR_TOOL_NAME, engineDetourFailureDiagnostic, engineNameFromEnv, isEngineDetourFailure, runEngineDetourOnce, } from "./engine-detour.js";
3
+ import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.js";
4
+ const engineDetourArgsSchema = Type.Object({
5
+ argv: Type.Array(Type.String({ minLength: 1 }), {
6
+ minItems: 1,
7
+ description: "Executable argv for one engine subprocess. First element is the command (PATH lookup); remaining elements are arguments. Build argv from the host CLI actual interface for the configured engine name; when optional packaged notes are present in the session prompt, follow those bytes. Do not invent package flags.",
8
+ }),
9
+ }, { additionalProperties: false });
10
+ /**
11
+ * Build one once-latch detour tool definition for a configured engine name.
12
+ * `latch` is shared so parent registration can reset between activations.
13
+ * `fail` owns host abort (parent) vs throw (evidence child).
14
+ */
15
+ export function createEngineDetourToolDefinition(input) {
16
+ const latch = input.latch ?? { used: false };
17
+ const engineName = input.engineName;
18
+ return wrapPackageOwnedToolDefinition({
19
+ name: ENGINE_DETOUR_TOOL_NAME,
20
+ label: "Engine Detour",
21
+ description: `Run one labor-engine subprocess (engine=${engineName}) and return its stdout to this session. Call at most once per activation. Build argv from the host CLI actual interface for this engine name; when optional packaged notes are present in the session prompt, follow those bytes too.`,
22
+ promptSnippet: "Run the configured labor engine once and return its stdout",
23
+ promptGuidelines: [
24
+ `Use ${ENGINE_DETOUR_TOOL_NAME} exactly once for the configured engine (${engineName}). Optional packaged notes are guidance when present; a bare engine name alone is also a valid call path.`,
25
+ "Pass argv for the host CLI of this engine name — first element is the executable name on PATH. Follow optional packaged notes when delivered; otherwise act from the engine name and the host CLI actual interface. Do not invent package flags.",
26
+ "On success, use the returned stdout as labor content for the existing typed submission / report path.",
27
+ ],
28
+ parameters: engineDetourArgsSchema,
29
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
30
+ if (latch.used) {
31
+ input.fail(new Error(ENGINE_DETOUR_ALREADY_USED_DIAGNOSTIC), toolCallId, ctx);
32
+ }
33
+ latch.used = true;
34
+ const args = params;
35
+ const argv = Array.isArray(args.argv) ? args.argv : [];
36
+ if (argv.length === 0 || argv.some((part) => typeof part !== "string" || part.length === 0)) {
37
+ input.fail(new Error("engine detour argv must be a non-empty string array"), toolCallId, ctx);
38
+ }
39
+ let result;
40
+ try {
41
+ result = await runEngineDetourOnce({
42
+ argv,
43
+ cwd: ctx.cwd,
44
+ ...(signal === undefined ? {} : { signal }),
45
+ });
46
+ }
47
+ catch (error) {
48
+ input.fail(error instanceof Error ? error : new Error(String(error)), toolCallId, ctx);
49
+ }
50
+ if (isEngineDetourFailure(result)) {
51
+ input.fail(new Error(engineDetourFailureDiagnostic(result)), toolCallId, ctx);
52
+ }
53
+ return {
54
+ content: [{ type: "text", text: result.stdout }],
55
+ details: {
56
+ tool: ENGINE_DETOUR_TOOL_NAME,
57
+ code: result.code,
58
+ },
59
+ };
60
+ },
61
+ });
62
+ }
63
+ /**
64
+ * Register the engine-generic detour tool once for this process when Judge/Reviewer has
65
+ * an engine activation signal. Returns whether registration occurred.
66
+ * Once-latch is activation-scoped via the returned reset handle.
67
+ */
68
+ export function registerEngineDetourTool(pi, hostActions) {
69
+ const engineName = engineNameFromEnv();
70
+ if (engineName === undefined) {
71
+ return {
72
+ registered: false,
73
+ resetLatch() {
74
+ /* no-op when unregistered */
75
+ },
76
+ };
77
+ }
78
+ const latch = { used: false };
79
+ const definition = createEngineDetourToolDefinition({
80
+ engineName,
81
+ latch,
82
+ fail(error, toolCallId, ctx) {
83
+ hostActions.failInfrastructure(error, ctx, toolCallId);
84
+ },
85
+ });
86
+ pi.registerTool(definition);
87
+ return {
88
+ registered: true,
89
+ resetLatch() {
90
+ latch.used = false;
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Engine-generic one-shot subprocess detour (#357 T2 / ADR 0069).
3
+ * Spawn once; no retry, hang surface, or per-engine branch.
4
+ * Material body is LLM data — this module only executes argv the model assembled.
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ /** Package-owned detour tool name (settlement whitelist + session principal). */
8
+ export const ENGINE_DETOUR_TOOL_NAME = "ak_engine_detour";
9
+ /** Env presence/name signal injected by public Judge run (registration gate only). */
10
+ export const AK_ROLE_ENGINE_ENV = "AK_ROLE_ENGINE";
11
+ export const ENGINE_DETOUR_EMPTY_STDOUT_DIAGNOSTIC = "engine detour produced empty stdout";
12
+ export const ENGINE_DETOUR_ALREADY_USED_DIAGNOSTIC = "engine detour already used in this activation";
13
+ /**
14
+ * Run one engine subprocess. First argv element is the executable (PATH lookup).
15
+ * stdio: ignore stdin, pipe stdout+stderr. No shell, no retry, no hang timer.
16
+ */
17
+ export async function runEngineDetourOnce(input) {
18
+ if (input.argv.length === 0) {
19
+ throw new Error("engine detour argv must be non-empty");
20
+ }
21
+ const command = input.argv[0];
22
+ const args = input.argv.slice(1);
23
+ return await new Promise((resolve, reject) => {
24
+ let settled = false;
25
+ const child = spawn(command, args, {
26
+ cwd: input.cwd,
27
+ env: input.env ?? process.env,
28
+ stdio: ["ignore", "pipe", "pipe"],
29
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
30
+ });
31
+ let stdout = "";
32
+ let stderr = "";
33
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
34
+ stdout += chunk;
35
+ });
36
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
37
+ stderr += chunk;
38
+ });
39
+ const fail = (error) => {
40
+ if (settled)
41
+ return;
42
+ settled = true;
43
+ reject(error instanceof Error ? error : new Error(String(error)));
44
+ };
45
+ child.on("error", (error) => fail(error));
46
+ child.on("close", (code) => {
47
+ if (settled)
48
+ return;
49
+ settled = true;
50
+ resolve({ code: code ?? 1, stdout, stderr });
51
+ });
52
+ });
53
+ }
54
+ /** Failure predicate: nonzero exit OR stdout trim-empty (including whitespace-only). */
55
+ export function isEngineDetourFailure(result) {
56
+ return result.code !== 0 || result.stdout.trim() === "";
57
+ }
58
+ /**
59
+ * Diagnostic string for shared settlement / Terminal Error Artifact.
60
+ * Prefer engine stderr 原样; whitespace-only/empty stderr is absent → stable fallback.
61
+ */
62
+ export function engineDetourFailureDiagnostic(result) {
63
+ if (result.stderr.trim().length > 0)
64
+ return result.stderr;
65
+ if (result.stdout.trim() === "")
66
+ return ENGINE_DETOUR_EMPTY_STDOUT_DIAGNOSTIC;
67
+ return `engine detour exited with code ${result.code}`;
68
+ }
69
+ /** Non-empty trimmed engine name from process.env, else undefined. */
70
+ export function engineNameFromEnv() {
71
+ const raw = process.env[AK_ROLE_ENGINE_ENV];
72
+ if (typeof raw !== "string")
73
+ return undefined;
74
+ const trimmed = raw.trim();
75
+ return trimmed === "" ? undefined : trimmed;
76
+ }
@@ -8,19 +8,23 @@ import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { createAssistantMessageEventStream, InMemoryCredentialStore, } from "@earendil-works/pi-ai";
10
10
  import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, prepareComplianceDispatch, } from "./compliance-transport.js";
11
+ import { createEngineDetourToolDefinition } from "./engine-detour-tool.js";
12
+ import { engineNameFromEnv } from "./engine-detour.js";
13
+ import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, } from "./package-resources/engine-material.js";
11
14
  import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.js";
12
15
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
13
16
  import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.js";
14
17
  import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.js";
15
18
  import { hasUpstreamErrorTestimony, isNonSuccessHttpStatus, projectConfirmedRemotePayload, } from "./upstream-error-testimony.js";
16
19
  /** Package-owned system prompt for Reviewer Standards/Spec evidence children (private carrier). */
17
- function buildEvidenceChildSystemPrompt() {
18
- return [
20
+ function buildEvidenceChildSystemPrompt(engineMaterial) {
21
+ const lines = [
19
22
  "Work only in the supplied workspace.",
20
23
  "Use the available evidence tools to investigate. Do not commit, push, or mutate remotes.",
21
24
  REVIEWER_VERIFICATION_BOUNDARY,
22
25
  "Return one substantive non-blank report.",
23
- ].join("\n");
26
+ ];
27
+ return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
24
28
  }
25
29
  // ── shared constants / types ──────────────────────────────────────────────
26
30
  export const AUDITOR_TURN_LIMIT = 32;
@@ -456,6 +460,30 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
456
460
  catch (error) {
457
461
  throw classifiedError(error, "provider");
458
462
  }
463
+ // #378: when labor engine is configured, legs get the same detour tool + material
464
+ // dual-path as the parent seat (ADR 0069 detour-rejoins-main-road).
465
+ const engineName = engineNameFromEnv();
466
+ const engineMaterial = engineName === undefined
467
+ ? undefined
468
+ : options.packageRoot === undefined || options.packageRoot.trim() === ""
469
+ // Name-only when package root is unavailable (still a valid #376 path).
470
+ ? Object.freeze({ name: engineName })
471
+ : engineSessionMaterialFromOptions({
472
+ engine: engineName,
473
+ packageRoot: options.packageRoot,
474
+ });
475
+ // #378: detour failure is durable for this leg — later assistant reports cannot wash it.
476
+ let engineDetourFailure;
477
+ const engineDetourTool = engineName === undefined
478
+ ? undefined
479
+ : createEngineDetourToolDefinition({
480
+ engineName,
481
+ fail(error) {
482
+ const failure = error instanceof Error ? error : new Error(String(error));
483
+ engineDetourFailure = failure;
484
+ throw failure;
485
+ },
486
+ });
459
487
  // No tools allowlist — Pi defaults + unrestricted evidence surface (ADR 0064).
460
488
  // Single createAgentSession owner: in-process-session.ts.
461
489
  const { session, dispose } = await openInProcessAgentSession({
@@ -464,7 +492,10 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
464
492
  model: inherited.model,
465
493
  thinkingLevel: context.thinkingLevel ?? "off",
466
494
  modelRuntime: inherited.runtime,
467
- systemPrompt: buildEvidenceChildSystemPrompt(),
495
+ systemPrompt: buildEvidenceChildSystemPrompt(engineMaterial),
496
+ ...(engineDetourTool === undefined
497
+ ? {}
498
+ : { customTools: [engineDetourTool] }),
468
499
  sessionManager: createRecordSession({
469
500
  cwd: workspace,
470
501
  kind: "evidence-children",
@@ -489,8 +520,18 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
489
520
  await session.prompt(delivered);
490
521
  }
491
522
  catch (error) {
523
+ // Engine detour fail becomes isError toolResult and must outrank a later
524
+ // provider-shaped throw from the same prompt turn (#378).
525
+ if (engineDetourFailure !== undefined) {
526
+ throw classifiedError(engineDetourFailure, "child");
527
+ }
492
528
  throw classifiedError(error, "provider");
493
529
  }
530
+ // Launched-leg detour non-zero / empty stdout / spawn failure is infrastructure:
531
+ // reject the leg even when the model still emits a non-blank report afterward.
532
+ if (engineDetourFailure !== undefined) {
533
+ throw classifiedError(engineDetourFailure, "child");
534
+ }
494
535
  if (signal?.aborted)
495
536
  throw new Error("Evidence child was cancelled");
496
537
  const lastAssistant = [...session.messages]
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Packaged engine method-material seam (#356 T1 / ADR 0069 / #376).
3
+ * Engine names are owner pool-directive labels — not a closed material catalog.
4
+ * Material body is optional data for the LLM, not a code contract.
5
+ * Only path-safety syntax is checked at real I/O seams.
6
+ */
7
+ import { existsSync, readdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ const ENGINE_MATERIAL_RELATIVE_ROOT = "resources/engines";
10
+ /** Non-empty, trimmed; reject only real path hazards at the I/O seam. */
11
+ export function isEngineNameSyntax(name) {
12
+ if (typeof name !== "string")
13
+ return false;
14
+ if (name.length === 0 || name.trim() !== name)
15
+ return false;
16
+ // Exact "." / ".." are directory aliases; consecutive dots inside a label are not.
17
+ if (name === "." || name === "..")
18
+ return false;
19
+ if (name.includes("/") || name.includes("\\") || name.includes("\0"))
20
+ return false;
21
+ return true;
22
+ }
23
+ export function engineMaterialRelativeDirectory() {
24
+ return ENGINE_MATERIAL_RELATIVE_ROOT;
25
+ }
26
+ export function resolveEngineMaterialDirectory(packageRoot) {
27
+ return join(packageRoot, ENGINE_MATERIAL_RELATIVE_ROOT);
28
+ }
29
+ /**
30
+ * Enumerate packaged engine notes stems (discovery only — not a legal-name gate).
31
+ * Only `*.md` stems that pass name syntax are listed.
32
+ */
33
+ export function listEngineMaterialNames(packageRoot) {
34
+ const dir = resolveEngineMaterialDirectory(packageRoot);
35
+ if (!existsSync(dir))
36
+ return Object.freeze([]);
37
+ const names = readdirSync(dir)
38
+ .filter((entry) => entry.endsWith(".md"))
39
+ .map((entry) => entry.slice(0, -".md".length))
40
+ .filter((stem) => isEngineNameSyntax(stem))
41
+ .sort();
42
+ return Object.freeze([...names]);
43
+ }
44
+ /**
45
+ * Build the packaged notes path for a syntax-legal engine name.
46
+ * Does not require the file to exist (material is optional data).
47
+ */
48
+ export function resolveEngineMaterialPath(packageRoot, name) {
49
+ const legal = assertLegalEngineName(name);
50
+ return join(resolveEngineMaterialDirectory(packageRoot), `${legal}.md`);
51
+ }
52
+ /**
53
+ * Path-safety syntax gate for engine labels at real I/O seams.
54
+ * Returns the canonical name on success; throws Error on illegal syntax.
55
+ * Does not consult any material catalog (ADR 0069 pool-directive axis).
56
+ */
57
+ export function assertLegalEngineName(name) {
58
+ if (!isEngineNameSyntax(name)) {
59
+ throw new Error(`illegal engine name: ${name}`);
60
+ }
61
+ return name;
62
+ }
63
+ /**
64
+ * Resolve optional engine options into session material coordinates.
65
+ * No engine → undefined (caller keeps default prompt bytes).
66
+ * Engine with packaged notes → name + absolute material path.
67
+ * Engine without notes → name only (pass-through; no warning).
68
+ */
69
+ export function engineSessionMaterialFromOptions(options) {
70
+ if (options.engine === undefined)
71
+ return undefined;
72
+ if (options.packageRoot === undefined || options.packageRoot.trim() === "") {
73
+ throw new Error("packageRoot is required when engine is configured");
74
+ }
75
+ const name = assertLegalEngineName(options.engine);
76
+ const materialPath = resolveEngineMaterialPath(options.packageRoot, name);
77
+ if (existsSync(materialPath)) {
78
+ return Object.freeze({ name, materialPath });
79
+ }
80
+ return Object.freeze({ name });
81
+ }
82
+ /**
83
+ * Append engine method-material delivery to session initial material lines.
84
+ * No engine → identity copy (byte-stable when joined the same way).
85
+ * With notes → read-these-bytes header + engine name + absolute material path.
86
+ * Name only → engine name coordinate only (no read-these-bytes header, no path, no warning).
87
+ * Never delivers material body.
88
+ */
89
+ export function appendEngineSessionMaterial(lines, engineMaterial) {
90
+ if (engineMaterial === undefined) {
91
+ return [...lines];
92
+ }
93
+ const out = [...lines];
94
+ out.push("");
95
+ if (engineMaterial.materialPath !== undefined) {
96
+ out.push("Engine method material (read these bytes and follow them):");
97
+ out.push(`- engine: ${engineMaterial.name}`);
98
+ out.push(`- ${engineMaterial.materialPath}`);
99
+ }
100
+ else {
101
+ // Name-only pass-through: no packaged bytes to read.
102
+ out.push(`- engine: ${engineMaterial.name}`);
103
+ }
104
+ return out;
105
+ }