@principles/codex-adapter 0.2.0 → 0.2.1

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/pd-hook.js CHANGED
@@ -109,7 +109,7 @@ export async function processHookInvocation(rawStdin, _env = process.env, cwd =
109
109
  }
110
110
  try {
111
111
  if (event.kind === 'session_start') {
112
- const health = await createProductionHostRuntime().health(resolution.workspaceDir);
112
+ const health = await createProductionHostRuntime({ hostKind: 'codex' }).health(resolution.workspaceDir);
113
113
  if (!health.ok)
114
114
  return { stdout: {}, exitCode: 0, stderr: [diagnostic(health.reason ?? 'runtime_unhealthy', health.nextAction ?? 'Inspect the Workspace runtime.')] };
115
115
  return { stdout: adapter.encodeOutput({ decision: 'allow', source: event.source }, 'session_start'), exitCode: 0, stderr: [] };
@@ -117,7 +117,7 @@ export async function processHookInvocation(rawStdin, _env = process.env, cwd =
117
117
  const ingestionDiagnostics = ingestionEnabled
118
118
  ? await runConversationIngestion({ rawPayload: parsed, kind: event.kind, workspaceDir: resolution.workspaceDir, env: _env })
119
119
  : [];
120
- const result = await createProductionHostRuntime().dispatch(event);
120
+ const result = await createProductionHostRuntime({ hostKind: 'codex' }).dispatch(event);
121
121
  const stderr = [...(result.warnings ?? []).slice(0, 16).map((warning) => diagnostic(warning, 'Inspect PD Workspace state and retry; the hook failed open.')), ...ingestionDiagnostics];
122
122
  return { stdout: adapter.encodeOutput(result, event.kind), exitCode: 0, stderr };
123
123
  }
@@ -11,6 +11,8 @@ export interface CodexWorkerCycleStepReport {
11
11
  readonly status: 'succeeded' | 'failed' | 'retried' | 'skipped' | 'degraded';
12
12
  readonly message?: string;
13
13
  readonly errorCategory?: string;
14
+ /** PRI-638 P1-C: recovery action for capability-disabled pauses. */
15
+ readonly nextAction?: string;
14
16
  } | null;
15
17
  readonly downstream: InternalizationConsumerCycleOutcome | null;
16
18
  }
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import fs from 'node:fs';
25
25
  import path from 'node:path';
26
- import { computeFeatureFlagsFromConfig, createRuntimeStateHandle, createPainSignalBridge, isRetryWaitBackoffElapsed, PrincipleTreeLedgerAdapter, } from '@principles/core/runtime-v2';
26
+ import { computeFeatureFlagsFromConfig, createRuntimeStateHandle, createPainSignalBridge, isRetryWaitBackoffElapsed, PrincipleTreeLedgerAdapter, resolveDiagnosticianCapability, } from '@principles/core/runtime-v2';
27
27
  import { loadPdConfigForPlugin, loadFeatureFlagFromConfig, reconcileGovernanceContinuation, runInternalizationConsumerCycle, } from '@principles/host-runtime';
28
28
  import { catchUpCodexIngestion } from '../ingestion/catch-up.js';
29
29
  const WORKER_OWNER = 'companion-worker';
@@ -86,6 +86,15 @@ export async function runCodexWorkspaceWorkerCycle(options) {
86
86
  return { ...base, mode: 'paused', reason: 'host.codex_disabled', nextAction: 'Set features.host.codex.enabled=true in the Workspace .pd/config.yaml to enable Codex PD behavior.' };
87
87
  }
88
88
  const consumerFlag = loadFeatureFlagFromConfig(workspaceDir, 'internalization_auto_consumer', { info: (m) => logger.info(m), warn: (m) => logger.warn(m) });
89
+ // PRI-638 P1-C: the Diagnostician capability gate is resolved from the same
90
+ // canonical authority the runtime factory uses. An Owner-disabled agent is
91
+ // an intentional governance state: the worker must NOT pick pending tasks,
92
+ // must NOT invoke any runner (provider calls = 0), must NOT report the
93
+ // workspace degraded, and must keep catch-up / reconciliation / downstream
94
+ // running. Pending tasks stay untouched and resume on a later cycle after
95
+ // the Owner re-enables the agent.
96
+ const capability = resolveDiagnosticianCapability(config.effective);
97
+ const diagnosticianDisabled = !capability.available;
89
98
  // Step 2 — Catch-up transcript lag (SPEC §13: gated by the ingestion flag,
90
99
  // NOT by the consumer flag; catchUpCodexIngestion re-checks and returns a
91
100
  // zero-I/O skip when ingestion is off).
@@ -99,7 +108,18 @@ export async function runCodexWorkspaceWorkerCycle(options) {
99
108
  const reconcile = await reconcileGovernanceContinuation({ workspaceDir });
100
109
  // Steps 4–6 — execution authority: internalization_auto_consumer.
101
110
  let diagnostician = null;
102
- if (!consumerFlag.enabled) {
111
+ if (diagnosticianDisabled) {
112
+ // PRI-638 P1-C: intentional governance pause. No candidate picking, no
113
+ // provider call, no degraded mode. The rest of the cycle (downstream)
114
+ // continues below; pending tasks resume once the Owner re-enables.
115
+ diagnostician = {
116
+ taskId: '',
117
+ status: 'skipped',
118
+ message: 'capability_disabled',
119
+ nextAction: capability.available ? undefined : capability.nextAction,
120
+ };
121
+ }
122
+ else if (!consumerFlag.enabled) {
103
123
  // paused: execution pause, NOT evidence freeze. Catch-up + reconcile
104
124
  // already ran above; manual CLI remains allowed (SPEC §13).
105
125
  return {
@@ -110,63 +130,68 @@ export async function runCodexWorkspaceWorkerCycle(options) {
110
130
  report: { catchUp, reconcile, diagnostician: null, downstream: null },
111
131
  };
112
132
  }
113
- // Step 4 — expired-lease recovery sweep, then Step 5/6: at most one
114
- // Diagnostician task via the existing bridge lease/runner contract.
115
- try {
116
- const handle = await createRuntimeStateHandle({ workspaceDir, readonly: false });
133
+ else {
134
+ // Step 4 expired-lease recovery sweep, then Step 5/6: at most one
135
+ // Diagnostician task via the existing bridge lease/runner contract.
136
+ // (Reached only when the Diagnostician capability is enabled AND the
137
+ // consumer flag is on — an intentional capability pause never reaches
138
+ // candidate picking or any provider call.)
117
139
  try {
118
- const sweep = await handle.stateManager.runRecoverySweep();
119
- if (sweep.recovered > 0 || sweep.errors.length > 0) {
120
- emitEvent('CODEX_WORKER_RECOVERY_SWEEP', JSON.stringify({ recovered: sweep.recovered, failed: sweep.errors.length }));
121
- }
122
- const candidate = await pickDiagnosticianCandidate(handle.stateManager, options.diagnosticianCandidateLimit ?? DEFAULT_DIAG_CANDIDATE_LIMIT);
123
- if (candidate !== null) {
124
- const stateDir = path.join(workspaceDir, '.state');
125
- const bridge = await createPainSignalBridge({
126
- workspaceDir,
127
- stateDir,
128
- ledgerAdapter: new PrincipleTreeLedgerAdapter({ stateDir }),
129
- owner: WORKER_OWNER,
130
- effectiveConfig: config.effective,
131
- getEnvVar: (name) => process.env[name],
132
- });
133
- const executed = await bridge.executePendingDiagnosis({ taskId: candidate.taskId });
134
- diagnostician = {
135
- taskId: candidate.taskId,
136
- status: executed.status,
137
- ...(executed.message !== undefined ? { message: executed.message.slice(0, 200) } : {}),
138
- ...(executed.errorCategory !== undefined ? { errorCategory: executed.errorCategory } : {}),
139
- };
140
- // The bridge stays cached per workspace (bounded: one per workspace
141
- // per worker process, exactly like the OpenClaw plugin host) — no
142
- // per-cycle dispose, so a concurrent cycle can never hit a disposed
143
- // bridge. The factory self-disposes losing concurrent constructions.
144
- }
145
- else {
146
- // No eligible candidate, but a retry_wait task may still be inside
147
- // its backoff window — report it so the cycle is observable instead
148
- // of looking like "nothing pending" (review P1: head-of-line).
149
- const waiting = await handle.stateManager.listTasks({ taskKind: 'diagnostician', status: 'retry_wait', orderBy: 'updated_at_asc', limit: 1 });
150
- const [oldestWaiting] = waiting;
151
- if (oldestWaiting !== undefined && !isRetryWaitBackoffElapsed(oldestWaiting.status, oldestWaiting.leaseExpiresAt)) {
152
- diagnostician = { taskId: oldestWaiting.taskId, status: 'skipped', message: 'retry_wait_pending' };
140
+ const handle = await createRuntimeStateHandle({ workspaceDir, readonly: false });
141
+ try {
142
+ const sweep = await handle.stateManager.runRecoverySweep();
143
+ if (sweep.recovered > 0 || sweep.errors.length > 0) {
144
+ emitEvent('CODEX_WORKER_RECOVERY_SWEEP', JSON.stringify({ recovered: sweep.recovered, failed: sweep.errors.length }));
145
+ }
146
+ const candidate = await pickDiagnosticianCandidate(handle.stateManager, options.diagnosticianCandidateLimit ?? DEFAULT_DIAG_CANDIDATE_LIMIT);
147
+ if (candidate !== null) {
148
+ const stateDir = path.join(workspaceDir, '.state');
149
+ const bridge = await createPainSignalBridge({
150
+ workspaceDir,
151
+ stateDir,
152
+ ledgerAdapter: new PrincipleTreeLedgerAdapter({ stateDir }),
153
+ owner: WORKER_OWNER,
154
+ effectiveConfig: config.effective,
155
+ getEnvVar: (name) => process.env[name],
156
+ });
157
+ const executed = await bridge.executePendingDiagnosis({ taskId: candidate.taskId });
158
+ diagnostician = {
159
+ taskId: candidate.taskId,
160
+ status: executed.status,
161
+ ...(executed.message !== undefined ? { message: executed.message.slice(0, 200) } : {}),
162
+ ...(executed.errorCategory !== undefined ? { errorCategory: executed.errorCategory } : {}),
163
+ };
164
+ // The bridge stays cached per workspace (bounded: one per workspace
165
+ // per worker process, exactly like the OpenClaw plugin host) — no
166
+ // per-cycle dispose, so a concurrent cycle can never hit a disposed
167
+ // bridge. The factory self-disposes losing concurrent constructions.
168
+ }
169
+ else {
170
+ // No eligible candidate, but a retry_wait task may still be inside
171
+ // its backoff window report it so the cycle is observable instead
172
+ // of looking like "nothing pending" (review P1: head-of-line).
173
+ const waiting = await handle.stateManager.listTasks({ taskKind: 'diagnostician', status: 'retry_wait', orderBy: 'updated_at_asc', limit: 1 });
174
+ const [oldestWaiting] = waiting;
175
+ if (oldestWaiting !== undefined && !isRetryWaitBackoffElapsed(oldestWaiting.status, oldestWaiting.leaseExpiresAt)) {
176
+ diagnostician = { taskId: oldestWaiting.taskId, status: 'skipped', message: 'retry_wait_pending' };
177
+ }
153
178
  }
154
179
  }
180
+ finally {
181
+ await handle.close().catch(() => undefined);
182
+ }
155
183
  }
156
- finally {
157
- await handle.close().catch(() => undefined);
184
+ catch (error) {
185
+ const detail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
186
+ return {
187
+ ...base,
188
+ mode: 'degraded',
189
+ reason: `diagnostician_execution_failed:${detail}`,
190
+ nextAction: 'Inspect the Workspace runtime profile and provider configuration; the task keeps its pending/retry state and no evidence was mutated.',
191
+ report: { catchUp, reconcile, diagnostician: null, downstream: null },
192
+ };
158
193
  }
159
194
  }
160
- catch (error) {
161
- const detail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
162
- return {
163
- ...base,
164
- mode: 'degraded',
165
- reason: `diagnostician_execution_failed:${detail}`,
166
- nextAction: 'Inspect the Workspace runtime profile and provider configuration; the task keeps its pending/retry state and no evidence was mutated.',
167
- report: { catchUp, reconcile, diagnostician: null, downstream: null },
168
- };
169
- }
170
195
  // Step 7 — ONE bounded downstream consumer cycle via the shared executor
171
196
  // (same implementation the OpenClaw auto-consumer runs).
172
197
  const downstream = await runInternalizationConsumerCycle(workspaceDir, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/codex-adapter",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",