create-principles-disciple 1.132.2 → 1.132.4

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.
Files changed (229) hide show
  1. package/codex-adapter/dist/codec/index.d.ts +6 -0
  2. package/codex-adapter/dist/codec/index.js +5 -0
  3. package/codex-adapter/dist/codec/input-decoder.d.ts +13 -0
  4. package/codex-adapter/dist/codec/input-decoder.js +103 -0
  5. package/codex-adapter/dist/codec/output-encoder.d.ts +32 -0
  6. package/codex-adapter/dist/codec/output-encoder.js +51 -0
  7. package/codex-adapter/dist/host-adapter.d.ts +30 -0
  8. package/codex-adapter/dist/host-adapter.js +28 -0
  9. package/codex-adapter/dist/index.d.ts +25 -0
  10. package/codex-adapter/dist/index.js +19 -0
  11. package/codex-adapter/dist/ingestion/admission.d.ts +54 -0
  12. package/codex-adapter/dist/ingestion/admission.js +137 -0
  13. package/codex-adapter/dist/ingestion/catch-up.d.ts +41 -0
  14. package/codex-adapter/dist/ingestion/catch-up.js +94 -0
  15. package/codex-adapter/dist/ingestion/codex-home.d.ts +19 -0
  16. package/codex-adapter/dist/ingestion/codex-home.js +43 -0
  17. package/codex-adapter/dist/ingestion/codex-version.d.ts +30 -0
  18. package/codex-adapter/dist/ingestion/codex-version.js +53 -0
  19. package/codex-adapter/dist/ingestion/ingestion-fields.d.ts +17 -0
  20. package/codex-adapter/dist/ingestion/ingestion-fields.js +28 -0
  21. package/codex-adapter/dist/ingestion/ingestion.d.ts +71 -0
  22. package/codex-adapter/dist/ingestion/ingestion.js +248 -0
  23. package/codex-adapter/dist/ingestion/transcript-decoder.d.ts +72 -0
  24. package/codex-adapter/dist/ingestion/transcript-decoder.js +485 -0
  25. package/codex-adapter/dist/ingestion/transcript-locate.d.ts +13 -0
  26. package/codex-adapter/dist/ingestion/transcript-locate.js +75 -0
  27. package/codex-adapter/dist/ingestion/transcript-path.d.ts +25 -0
  28. package/codex-adapter/dist/ingestion/transcript-path.js +87 -0
  29. package/codex-adapter/dist/pd-hook.d.ts +9 -0
  30. package/codex-adapter/dist/pd-hook.js +159 -0
  31. package/codex-adapter/dist/tool-semantics.d.ts +20 -0
  32. package/codex-adapter/dist/tool-semantics.js +27 -0
  33. package/codex-adapter/dist/worker/workspace-worker.d.ts +59 -0
  34. package/codex-adapter/dist/worker/workspace-worker.js +270 -0
  35. package/codex-adapter/package.json +52 -0
  36. package/console/dist/server/routes/update.js +187 -19
  37. package/console/dist/server/update/mutation-controller.d.ts +18 -0
  38. package/console/dist/server/update/mutation-controller.js +29 -0
  39. package/console/dist/server/utils/installed-layout.d.ts +1 -0
  40. package/console/dist/server/utils/installed-layout.js +2 -0
  41. package/console/package.json +1 -0
  42. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +15 -0
  43. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  44. package/core/dist/runtime-v2/adapter/__tests__/pd-stream-simple.test.d.ts +2 -0
  45. package/core/dist/runtime-v2/adapter/__tests__/pd-stream-simple.test.d.ts.map +1 -0
  46. package/core/dist/runtime-v2/adapter/__tests__/pd-stream-simple.test.js +71 -0
  47. package/core/dist/runtime-v2/adapter/__tests__/pd-stream-simple.test.js.map +1 -0
  48. package/core/dist/runtime-v2/adapter/__tests__/pi-ai-http-transport.test.d.ts +2 -0
  49. package/core/dist/runtime-v2/adapter/__tests__/pi-ai-http-transport.test.d.ts.map +1 -0
  50. package/core/dist/runtime-v2/adapter/__tests__/pi-ai-http-transport.test.js +219 -0
  51. package/core/dist/runtime-v2/adapter/__tests__/pi-ai-http-transport.test.js.map +1 -0
  52. package/core/dist/runtime-v2/adapter/artificer-l2-adapter.d.ts.map +1 -1
  53. package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js +2 -3
  54. package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js.map +1 -1
  55. package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.d.ts +12 -1
  56. package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.d.ts.map +1 -1
  57. package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js +15 -2
  58. package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js.map +1 -1
  59. package/core/dist/runtime-v2/adapter/pi-ai-http-transport.d.ts +50 -0
  60. package/core/dist/runtime-v2/adapter/pi-ai-http-transport.d.ts.map +1 -0
  61. package/core/dist/runtime-v2/adapter/pi-ai-http-transport.js +93 -0
  62. package/core/dist/runtime-v2/adapter/pi-ai-http-transport.js.map +1 -0
  63. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
  64. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +5 -0
  65. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
  66. package/core/dist/runtime-v2/internalization/__tests__/owner-decision.test.js +152 -0
  67. package/core/dist/runtime-v2/internalization/__tests__/owner-decision.test.js.map +1 -1
  68. package/core/dist/runtime-v2/internalization/owner-review.d.ts +7 -1
  69. package/core/dist/runtime-v2/internalization/owner-review.d.ts.map +1 -1
  70. package/core/dist/runtime-v2/internalization/owner-review.js +22 -3
  71. package/core/dist/runtime-v2/internalization/owner-review.js.map +1 -1
  72. package/core/package.json +3 -2
  73. package/dist/installer.d.ts +32 -0
  74. package/dist/installer.d.ts.map +1 -1
  75. package/dist/installer.js +198 -2
  76. package/dist/installer.js.map +1 -1
  77. package/dist/installers/codex-host-installer.d.ts.map +1 -1
  78. package/dist/installers/codex-host-installer.js +5 -3
  79. package/dist/installers/codex-host-installer.js.map +1 -1
  80. package/dist/update/release-manager-authority.d.ts +69 -0
  81. package/dist/update/release-manager-authority.d.ts.map +1 -0
  82. package/dist/update/release-manager-authority.js +125 -0
  83. package/dist/update/release-manager-authority.js.map +1 -0
  84. package/dist/utils/workspace-divergence.d.ts +36 -0
  85. package/dist/utils/workspace-divergence.d.ts.map +1 -0
  86. package/dist/utils/workspace-divergence.js +87 -0
  87. package/dist/utils/workspace-divergence.js.map +1 -0
  88. package/install-layout/dist/index.d.ts +5 -0
  89. package/install-layout/dist/index.js +1 -0
  90. package/install-layout/package.json +1 -1
  91. package/package.json +4 -2
  92. package/plugin/dist/bundle.js +870 -734
  93. package/plugin/dist/governance-audit.js +381 -247
  94. package/plugin/dist/openclaw-sdk.d.ts +3 -0
  95. package/plugin/dist/rulehost-evidence.js +420 -286
  96. package/plugin/dist/service/subagent-workflow/types.d.ts +2 -2
  97. package/plugin/dist/utils/retry.d.ts +1 -1
  98. package/plugin/dist/utils/workspace-resolver.d.ts +35 -4
  99. package/plugin/openclaw.plugin.json +1 -1
  100. package/plugin/package.json +1 -1
  101. package/release-manager/dist/i18n.d.ts +6 -0
  102. package/release-manager/dist/i18n.d.ts.map +1 -0
  103. package/release-manager/dist/i18n.js +234 -0
  104. package/release-manager/dist/i18n.js.map +1 -0
  105. package/release-manager/dist/index.d.ts +7 -0
  106. package/release-manager/dist/index.d.ts.map +1 -0
  107. package/release-manager/dist/index.js +474 -0
  108. package/release-manager/dist/index.js.map +1 -0
  109. package/release-manager/dist/installer.d.ts +217 -0
  110. package/release-manager/dist/installer.d.ts.map +1 -0
  111. package/release-manager/dist/installer.js +2451 -0
  112. package/release-manager/dist/installer.js.map +1 -0
  113. package/release-manager/dist/installers/codex-host-installer.d.ts +29 -0
  114. package/release-manager/dist/installers/codex-host-installer.d.ts.map +1 -0
  115. package/release-manager/dist/installers/codex-host-installer.js +496 -0
  116. package/release-manager/dist/installers/codex-host-installer.js.map +1 -0
  117. package/release-manager/dist/installers/index.d.ts +40 -0
  118. package/release-manager/dist/installers/index.d.ts.map +1 -0
  119. package/release-manager/dist/installers/index.js +35 -0
  120. package/release-manager/dist/installers/index.js.map +1 -0
  121. package/release-manager/dist/installers/openclaw-host-installer.d.ts +18 -0
  122. package/release-manager/dist/installers/openclaw-host-installer.d.ts.map +1 -0
  123. package/release-manager/dist/installers/openclaw-host-installer.js +414 -0
  124. package/release-manager/dist/installers/openclaw-host-installer.js.map +1 -0
  125. package/release-manager/dist/mvp-config.d.ts +197 -0
  126. package/release-manager/dist/mvp-config.d.ts.map +1 -0
  127. package/release-manager/dist/mvp-config.js +509 -0
  128. package/release-manager/dist/mvp-config.js.map +1 -0
  129. package/release-manager/dist/prompts.d.ts +30 -0
  130. package/release-manager/dist/prompts.d.ts.map +1 -0
  131. package/release-manager/dist/prompts.js +231 -0
  132. package/release-manager/dist/prompts.js.map +1 -0
  133. package/release-manager/dist/skill-language.d.ts +32 -0
  134. package/release-manager/dist/skill-language.d.ts.map +1 -0
  135. package/release-manager/dist/skill-language.js +107 -0
  136. package/release-manager/dist/skill-language.js.map +1 -0
  137. package/release-manager/dist/uninstaller.d.ts +55 -0
  138. package/release-manager/dist/uninstaller.d.ts.map +1 -0
  139. package/release-manager/dist/uninstaller.js +409 -0
  140. package/release-manager/dist/uninstaller.js.map +1 -0
  141. package/release-manager/dist/update/atomic-file.d.ts +35 -0
  142. package/release-manager/dist/update/atomic-file.d.ts.map +1 -0
  143. package/release-manager/dist/update/atomic-file.js +120 -0
  144. package/release-manager/dist/update/atomic-file.js.map +1 -0
  145. package/release-manager/dist/update/bootstrap-protocol.d.ts +55 -0
  146. package/release-manager/dist/update/bootstrap-protocol.d.ts.map +1 -0
  147. package/release-manager/dist/update/bootstrap-protocol.js +132 -0
  148. package/release-manager/dist/update/bootstrap-protocol.js.map +1 -0
  149. package/release-manager/dist/update/channel-metadata.d.ts +28 -0
  150. package/release-manager/dist/update/channel-metadata.d.ts.map +1 -0
  151. package/release-manager/dist/update/channel-metadata.js +90 -0
  152. package/release-manager/dist/update/channel-metadata.js.map +1 -0
  153. package/release-manager/dist/update/channel-promotion.d.ts +39 -0
  154. package/release-manager/dist/update/channel-promotion.d.ts.map +1 -0
  155. package/release-manager/dist/update/channel-promotion.js +55 -0
  156. package/release-manager/dist/update/channel-promotion.js.map +1 -0
  157. package/release-manager/dist/update/data-compatibility.d.ts +35 -0
  158. package/release-manager/dist/update/data-compatibility.d.ts.map +1 -0
  159. package/release-manager/dist/update/data-compatibility.js +45 -0
  160. package/release-manager/dist/update/data-compatibility.js.map +1 -0
  161. package/release-manager/dist/update/install-layout.d.ts +59 -0
  162. package/release-manager/dist/update/install-layout.d.ts.map +1 -0
  163. package/release-manager/dist/update/install-layout.js +127 -0
  164. package/release-manager/dist/update/install-layout.js.map +1 -0
  165. package/release-manager/dist/update/legacy-migration.d.ts +56 -0
  166. package/release-manager/dist/update/legacy-migration.d.ts.map +1 -0
  167. package/release-manager/dist/update/legacy-migration.js +254 -0
  168. package/release-manager/dist/update/legacy-migration.js.map +1 -0
  169. package/release-manager/dist/update/product-identity.d.ts +50 -0
  170. package/release-manager/dist/update/product-identity.d.ts.map +1 -0
  171. package/release-manager/dist/update/product-identity.js +63 -0
  172. package/release-manager/dist/update/product-identity.js.map +1 -0
  173. package/release-manager/dist/update/release-asset-manifest.d.ts +31 -0
  174. package/release-manager/dist/update/release-asset-manifest.d.ts.map +1 -0
  175. package/release-manager/dist/update/release-asset-manifest.js +161 -0
  176. package/release-manager/dist/update/release-asset-manifest.js.map +1 -0
  177. package/release-manager/dist/update/release-identity.d.ts +33 -0
  178. package/release-manager/dist/update/release-identity.d.ts.map +1 -0
  179. package/release-manager/dist/update/release-identity.js +108 -0
  180. package/release-manager/dist/update/release-identity.js.map +1 -0
  181. package/release-manager/dist/update/release-manager-authority.d.ts +69 -0
  182. package/release-manager/dist/update/release-manager-authority.d.ts.map +1 -0
  183. package/release-manager/dist/update/release-manager-authority.js +125 -0
  184. package/release-manager/dist/update/release-manager-authority.js.map +1 -0
  185. package/release-manager/dist/update/release-manager.d.ts +96 -0
  186. package/release-manager/dist/update/release-manager.d.ts.map +1 -0
  187. package/release-manager/dist/update/release-manager.js +253 -0
  188. package/release-manager/dist/update/release-manager.js.map +1 -0
  189. package/release-manager/dist/update/release-metadata.d.ts +57 -0
  190. package/release-manager/dist/update/release-metadata.d.ts.map +1 -0
  191. package/release-manager/dist/update/release-metadata.js +200 -0
  192. package/release-manager/dist/update/release-metadata.js.map +1 -0
  193. package/release-manager/dist/update/release-policy.d.ts +51 -0
  194. package/release-manager/dist/update/release-policy.d.ts.map +1 -0
  195. package/release-manager/dist/update/release-policy.js +103 -0
  196. package/release-manager/dist/update/release-policy.js.map +1 -0
  197. package/release-manager/dist/update/rollback-policy.d.ts +56 -0
  198. package/release-manager/dist/update/rollback-policy.d.ts.map +1 -0
  199. package/release-manager/dist/update/rollback-policy.js +61 -0
  200. package/release-manager/dist/update/rollback-policy.js.map +1 -0
  201. package/release-manager/dist/update/transaction-journal.d.ts +131 -0
  202. package/release-manager/dist/update/transaction-journal.d.ts.map +1 -0
  203. package/release-manager/dist/update/transaction-journal.js +346 -0
  204. package/release-manager/dist/update/transaction-journal.js.map +1 -0
  205. package/release-manager/dist/update/trust-metadata.d.ts +55 -0
  206. package/release-manager/dist/update/trust-metadata.d.ts.map +1 -0
  207. package/release-manager/dist/update/trust-metadata.js +170 -0
  208. package/release-manager/dist/update/trust-metadata.js.map +1 -0
  209. package/release-manager/dist/update/update-history.d.ts +46 -0
  210. package/release-manager/dist/update/update-history.d.ts.map +1 -0
  211. package/release-manager/dist/update/update-history.js +123 -0
  212. package/release-manager/dist/update/update-history.js.map +1 -0
  213. package/release-manager/dist/utils/config-file-io.d.ts +26 -0
  214. package/release-manager/dist/utils/config-file-io.d.ts.map +1 -0
  215. package/release-manager/dist/utils/config-file-io.js +128 -0
  216. package/release-manager/dist/utils/config-file-io.js.map +1 -0
  217. package/release-manager/dist/utils/env.d.ts +60 -0
  218. package/release-manager/dist/utils/env.d.ts.map +1 -0
  219. package/release-manager/dist/utils/env.js +308 -0
  220. package/release-manager/dist/utils/env.js.map +1 -0
  221. package/release-manager/dist/utils/logger.d.ts +15 -0
  222. package/release-manager/dist/utils/logger.d.ts.map +1 -0
  223. package/release-manager/dist/utils/logger.js +30 -0
  224. package/release-manager/dist/utils/logger.js.map +1 -0
  225. package/release-manager/dist/utils/workspace-divergence.d.ts +36 -0
  226. package/release-manager/dist/utils/workspace-divergence.d.ts.map +1 -0
  227. package/release-manager/dist/utils/workspace-divergence.js +87 -0
  228. package/release-manager/dist/utils/workspace-divergence.js.map +1 -0
  229. package/release-manager/package.json +79 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Codex transcript path authorization (Codex Governance Closure SPEC rev 2
3
+ * §9; Slice A SPEC §11).
4
+ *
5
+ * The ONLY valid transcript source is the path explicitly supplied by the
6
+ * authenticated Codex hook — never a scan of `$CODEX_HOME/sessions`, never a
7
+ * "latest session" guess. Validation covers the real repository/OS path
8
+ * model: absolute path, `..` traversal rejection, canonical (realpath)
9
+ * normalization, containment under the resolved Codex home's `sessions`
10
+ * root at segment boundaries, symlink/junction escape rejection (realpath
11
+ * collapses them; containment is checked on the canonical form), and a
12
+ * regular-file requirement. The rollout identity — one physical transcript,
13
+ * distinct from the root session id (G1 §4 subagent/fork traps) — is the
14
+ * uuid embedded in the Codex rollout file name.
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { canonicalizePath } from './codex-home.js';
19
+ const UUID_HEX = /^[0-9a-fA-F]+$/;
20
+ function isHex(value) {
21
+ return UUID_HEX.test(value);
22
+ }
23
+ /** rollout-<timestamp>-<uuid>.jsonl — returns the rollout uuid, or null when the name is off-contract. */
24
+ export function parseRolloutFileName(fileName) {
25
+ if (!fileName.startsWith('rollout-') || !fileName.endsWith('.jsonl'))
26
+ return null;
27
+ const stem = fileName.slice('rollout-'.length, -'.jsonl'.length);
28
+ const parts = stem.split('-');
29
+ if (parts.length < 6)
30
+ return null; // at least one timestamp segment + the five uuid groups
31
+ const [a, b, c, d, e] = parts.slice(-5);
32
+ if (a === undefined || b === undefined || c === undefined || d === undefined || e === undefined)
33
+ return null;
34
+ if (a.length !== 8 || b.length !== 4 || c.length !== 4 || d.length !== 4 || e.length !== 12)
35
+ return null;
36
+ if (!isHex(a) || !isHex(b) || !isHex(c) || !isHex(d) || !isHex(e))
37
+ return null;
38
+ return parts.slice(-5).join('-').toLowerCase();
39
+ }
40
+ function isAbsolutePath(value) {
41
+ return path.isAbsolute(value);
42
+ }
43
+ function hasParentTraversal(value) {
44
+ return value.split(/[\\/]+/).includes('..');
45
+ }
46
+ function isContained(root, candidate) {
47
+ const relative = path.relative(root, candidate);
48
+ return relative === '' || (relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative));
49
+ }
50
+ export function validateCodexTranscriptPath(transcriptPath, codexHome) {
51
+ if (!isAbsolutePath(transcriptPath) || hasParentTraversal(transcriptPath)) {
52
+ return { ok: false, reason: 'transcript_path_invalid', nextAction: 'Codex must supply an absolute transcript_path without traversal segments.' };
53
+ }
54
+ const fileName = path.basename(transcriptPath);
55
+ const rolloutIdentity = parseRolloutFileName(fileName);
56
+ if (rolloutIdentity === null) {
57
+ return { ok: false, reason: 'transcript_path_invalid', nextAction: 'the transcript file name does not match the Codex rollout contract (rollout-<timestamp>-<uuid>.jsonl).' };
58
+ }
59
+ let canonicalHome;
60
+ let canonicalPath;
61
+ try {
62
+ // Canonicalize both sides to the final long form: Node's JS realpath
63
+ // preserves 8.3 short-name segments while payloads carry long forms.
64
+ canonicalHome = canonicalizePath(codexHome);
65
+ canonicalPath = canonicalizePath(transcriptPath);
66
+ }
67
+ catch {
68
+ return { ok: false, reason: 'transcript_path_invalid', nextAction: 'the transcript path (or the Codex home) does not resolve to an existing location.' };
69
+ }
70
+ // realpath collapses symlinks/junctions; checking containment on the
71
+ // canonical form rejects escapes that traverse links out of the sessions root.
72
+ if (!isContained(path.join(canonicalHome, 'sessions'), canonicalPath)) {
73
+ return { ok: false, reason: 'transcript_path_outside_codex_home', nextAction: 'the transcript is not inside the resolved CODEX_HOME sessions root; refusing to read outside the authenticated Codex home.' };
74
+ }
75
+ let stats;
76
+ try {
77
+ stats = fs.statSync(canonicalPath);
78
+ }
79
+ catch {
80
+ return { ok: false, reason: 'transcript_path_invalid', nextAction: 'the transcript file disappeared during validation.' };
81
+ }
82
+ if (!stats.isFile()) {
83
+ return { ok: false, reason: 'transcript_path_invalid', nextAction: 'the transcript path is not a regular file.' };
84
+ }
85
+ const identity = { dev: stats.dev, ino: Number(stats.ino), size: stats.size, mtimeMs: stats.mtimeMs };
86
+ return { ok: true, canonicalPath, rolloutIdentity: rolloutIdentity.toLowerCase(), identity };
87
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ type EnvMap = Record<string, string | undefined>;
3
+ export interface PdHookResult {
4
+ stdout: unknown;
5
+ exitCode: number;
6
+ stderr: string[];
7
+ }
8
+ export declare function processHookInvocation(rawStdin: string, _env?: EnvMap, cwd?: string): Promise<PdHookResult>;
9
+ export {};
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import process from 'node:process';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { createProductionHostRuntime, loadPdConfigForPlugin, resolveNearestPdWorkspace } from '@principles/host-runtime';
6
+ import { CODEX_TOOL_SEMANTICS } from './tool-semantics.js';
7
+ import { computeFeatureFlagsFromConfig } from '@principles/core/runtime-v2';
8
+ import { CodexHooksHostAdapter } from './host-adapter.js';
9
+ import { CodexDecoderError, CodexEncoderError } from './codec/index.js';
10
+ import { ingestCodexConversation } from './ingestion/ingestion.js';
11
+ import { runGovernanceAdmission } from './ingestion/admission.js';
12
+ const MAX_DIAGNOSTIC = 500;
13
+ function diagnostic(reason, nextAction) {
14
+ const boundedReason = reason.replace(/\s+/g, ' ').trim().slice(0, MAX_DIAGNOSTIC);
15
+ const boundedNextAction = nextAction.replace(/\s+/g, ' ').trim().slice(0, MAX_DIAGNOSTIC);
16
+ return `[PD] status=degraded reason=${boundedReason} nextAction=${boundedNextAction}`;
17
+ }
18
+ function errorMessage(error) {
19
+ return error instanceof Error ? error.message.slice(0, MAX_DIAGNOSTIC) : 'unknown_error';
20
+ }
21
+ // Bounded governance-observation ingestion (Codex Governance Closure Slice
22
+ // A) followed by the Slice B signal-admission pass (SPEC §12/§13): detection
23
+ // → canonical pain → evidence promotion → one pending Diagnostician task.
24
+ // Runs only when BOTH host.codex and codex_conversation_ingestion are
25
+ // enabled — the flag gate below happens BEFORE any transcript path
26
+ // validation or filesystem I/O, so flag-off means the transcript boundary
27
+ // receives zero calls (SPEC §10 hard privacy invariant). Admission runs
28
+ // BEFORE dispatch so a live tool failure is admitted through the same
29
+ // canonical derivation first and the production handler's duplicate probe
30
+ // then converges to a no-op (exactly one pain per real tool call).
31
+ async function runConversationIngestion(args) {
32
+ const { rawPayload, kind, workspaceDir, env } = args;
33
+ if (kind !== 'turn_complete' && kind !== 'before_prompt_build' && kind !== 'after_tool_call')
34
+ return [];
35
+ const diagnostics = [];
36
+ try {
37
+ const outcome = ingestCodexConversation(rawPayload, kind, { workspaceDir, env });
38
+ if (outcome.status === 'degraded') {
39
+ diagnostics.push(diagnostic(outcome.reason, outcome.nextAction));
40
+ }
41
+ else {
42
+ for (const warning of outcome.warnings.slice(0, 2)) {
43
+ diagnostics.push(diagnostic(warning, 'Inspect PD Workspace governance-observation state; ingestion continued.'));
44
+ }
45
+ // Slice B admission: hook awaits only admission + durable enqueue —
46
+ // never an LLM (SPEC §12/§13). Ordinary conversation returns no
47
+ // candidates/no admissions and stays completely silent.
48
+ const admission = await runGovernanceAdmission({
49
+ workspaceDir,
50
+ candidates: outcome.admissionCandidates,
51
+ });
52
+ for (const degradation of admission.degradations.slice(0, 2)) {
53
+ diagnostics.push(diagnostic(degradation.reason, degradation.nextAction));
54
+ }
55
+ }
56
+ }
57
+ catch (error) {
58
+ diagnostics.push(diagnostic(`codex_ingestion_unexpected:${errorMessage(error)}`, 'Retry the next Codex turn; if it repeats, inspect PD stderr and the Workspace trajectory database.'));
59
+ }
60
+ return diagnostics;
61
+ }
62
+ export async function processHookInvocation(rawStdin, _env = process.env, cwd = process.cwd()) {
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(rawStdin);
66
+ }
67
+ catch (error) {
68
+ return { stdout: {}, exitCode: 0, stderr: [diagnostic(`stdin_json_invalid:${errorMessage(error)}`, 'Verify Codex invokes the PD hook with one JSON object.')] };
69
+ }
70
+ const adapter = new CodexHooksHostAdapter();
71
+ let event;
72
+ try {
73
+ event = adapter.decodeEvent(parsed);
74
+ }
75
+ catch (error) {
76
+ const reason = error instanceof CodexDecoderError ? error.reason : `decode_threw:${errorMessage(error)}`;
77
+ const nextAction = error instanceof CodexDecoderError ? error.nextAction : 'Inspect the Codex 0.147 hook payload.';
78
+ return { stdout: {}, exitCode: 0, stderr: [diagnostic(reason, nextAction)] };
79
+ }
80
+ // Codex 0.147 supplies the invocation cwd. It is the authoritative starting
81
+ // point; a process-global environment variable can otherwise route one
82
+ // Workspace's hook into another Workspace's business state.
83
+ const requestedCwd = event.context.workspaceDir || cwd;
84
+ const resolution = resolveNearestPdWorkspace(requestedCwd);
85
+ if (!resolution.ok)
86
+ return { stdout: {}, exitCode: 0, stderr: [diagnostic(resolution.reason, resolution.nextAction)] };
87
+ event = { ...event, context: { ...event.context, workspaceDir: resolution.workspaceDir } };
88
+ const config = loadPdConfigForPlugin(resolution.workspaceDir);
89
+ if (!config.ok) {
90
+ const [first] = config.errors;
91
+ return { stdout: {}, exitCode: 0, stderr: [diagnostic(first?.reason ?? 'pd_config_invalid', first?.nextAction ?? 'Repair .pd/config.yaml.')] };
92
+ }
93
+ const { flags } = computeFeatureFlagsFromConfig(config.effective);
94
+ if (flags['host.codex']?.enabled !== true) {
95
+ return { stdout: {}, exitCode: 0, stderr: [diagnostic('host.codex_disabled', 'Set features.host.codex.enabled=true in the selected Workspace to enable PD.')] };
96
+ }
97
+ const ingestionEnabled = flags.codex_conversation_ingestion?.enabled === true;
98
+ if (event.kind === 'turn_complete') {
99
+ // Stop is the turn-complete ingestion trigger (G1 §2): no dispatch route,
100
+ // and Codex's Stop output schema has no hookSpecificOutput — the neutral
101
+ // result is exactly `{}` on stdout (runtime contract: "PD emits empty
102
+ // stdout on Stop"). Flag-off preserves the zero-transcript-read invariant
103
+ // and emits ONE bounded structured feature_disabled fact per completed
104
+ // turn on stderr — never stdout, and never on the per-tool events (that
105
+ // would be per-event noise).
106
+ const stderr = ingestionEnabled
107
+ ? await runConversationIngestion({ rawPayload: parsed, kind: event.kind, workspaceDir: resolution.workspaceDir, env: _env })
108
+ : [diagnostic('feature_disabled', 'Set features.codex_conversation_ingestion.enabled=true in the selected Workspace .pd/config.yaml to enable bounded conversation ingestion.')];
109
+ return { stdout: {}, exitCode: 0, stderr };
110
+ }
111
+ try {
112
+ if (event.kind === 'session_start') {
113
+ const health = await createProductionHostRuntime({ hostKind: 'codex' }).health(resolution.workspaceDir);
114
+ if (!health.ok)
115
+ return { stdout: {}, exitCode: 0, stderr: [diagnostic(health.reason ?? 'runtime_unhealthy', health.nextAction ?? 'Inspect the Workspace runtime.')] };
116
+ return { stdout: adapter.encodeOutput({ decision: 'allow', source: event.source }, 'session_start'), exitCode: 0, stderr: [] };
117
+ }
118
+ const ingestionDiagnostics = ingestionEnabled
119
+ ? await runConversationIngestion({ rawPayload: parsed, kind: event.kind, workspaceDir: resolution.workspaceDir, env: _env })
120
+ : [];
121
+ const result = await createProductionHostRuntime({ hostKind: 'codex', toolSemantics: CODEX_TOOL_SEMANTICS }).dispatch(event);
122
+ const stderr = [...(result.warnings ?? []).slice(0, 16).map((warning) => diagnostic(warning, 'Inspect PD Workspace state and retry; the hook failed open.')), ...ingestionDiagnostics];
123
+ return { stdout: adapter.encodeOutput(result, event.kind), exitCode: 0, stderr };
124
+ }
125
+ catch (error) {
126
+ const reason = error instanceof CodexEncoderError ? error.reason : `runtime_failed:${errorMessage(error)}`;
127
+ const nextAction = error instanceof CodexEncoderError ? error.nextAction : 'Inspect PD Workspace state and retry; the hook failed open.';
128
+ return { stdout: {}, exitCode: 0, stderr: [diagnostic(reason, nextAction)] };
129
+ }
130
+ }
131
+ async function main() {
132
+ let raw;
133
+ try {
134
+ raw = readFileSync(0, 'utf8');
135
+ }
136
+ catch (error) {
137
+ process.stderr.write(`${diagnostic(`stdin_read_failed:${errorMessage(error)}`, 'Run the hook from Codex with JSON stdin.')}\n`);
138
+ process.stdout.write('{}\n');
139
+ return;
140
+ }
141
+ let result;
142
+ try {
143
+ result = await processHookInvocation(raw);
144
+ }
145
+ catch (error) {
146
+ // Fail-open belt for an unexpected pre-dispatch throw (e.g. a workspace
147
+ // resolution race): Codex must still receive exactly one JSON object on
148
+ // stdout and a bounded diagnostic on stderr — never a bare crash.
149
+ process.stderr.write(`${diagnostic(`hook_pipeline_unexpected:${errorMessage(error)}`, 'Retry the tool call; if it repeats, inspect PD stderr and the Workspace .pd/config.yaml state.')}\n`);
150
+ result = { stdout: {}, exitCode: 0, stderr: [] };
151
+ }
152
+ for (const line of result.stderr)
153
+ process.stderr.write(`${line}\n`);
154
+ process.stdout.write(`${JSON.stringify(result.stdout)}\n`);
155
+ process.exitCode = result.exitCode;
156
+ }
157
+ const [, entry] = process.argv;
158
+ if (entry && import.meta.url === pathToFileURL(entry).href)
159
+ void main();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Codex Tool Semantic Declaration — PRI-634-F R2 (review P1-2)
3
+ *
4
+ * PURPOSE: the Codex host layer of the Tool Semantic Registry. Codex names
5
+ * its shell tool `Bash` (capitalized — evidenced by the codex-adapter
6
+ * integration fixtures), which the core baseline's lowercase `bash` never
7
+ * matches: rules generated against the baseline name could pass replay and
8
+ * never fire on a real Codex event.
9
+ *
10
+ * EVIDENCE BOUND: this declaration contains only names with in-repo
11
+ * evidence — the installer's hook matcher `Bash|apply_patch`
12
+ * (create-principles-disciple/src/installers/codex-host-installer.ts:203,
13
+ * locked by codex-plugin-bundle.test.ts). Extending it with the remaining
14
+ * Codex tool surface is tracked as PRI-657 — a wrong guess would be worse
15
+ * than a gap (the reliability check rejects undeclared names, forcing
16
+ * correct names).
17
+ */
18
+ import { type ToolSemanticMappingV1, type ToolSemanticRegistry } from '@principles/core/runtime-v2';
19
+ export declare const CODEX_TOOL_SEMANTIC_MAPPINGS: readonly ToolSemanticMappingV1[];
20
+ export declare const CODEX_TOOL_SEMANTICS: ToolSemanticRegistry;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Codex Tool Semantic Declaration — PRI-634-F R2 (review P1-2)
3
+ *
4
+ * PURPOSE: the Codex host layer of the Tool Semantic Registry. Codex names
5
+ * its shell tool `Bash` (capitalized — evidenced by the codex-adapter
6
+ * integration fixtures), which the core baseline's lowercase `bash` never
7
+ * matches: rules generated against the baseline name could pass replay and
8
+ * never fire on a real Codex event.
9
+ *
10
+ * EVIDENCE BOUND: this declaration contains only names with in-repo
11
+ * evidence — the installer's hook matcher `Bash|apply_patch`
12
+ * (create-principles-disciple/src/installers/codex-host-installer.ts:203,
13
+ * locked by codex-plugin-bundle.test.ts). Extending it with the remaining
14
+ * Codex tool surface is tracked as PRI-657 — a wrong guess would be worse
15
+ * than a gap (the reliability check rejects undeclared names, forcing
16
+ * correct names).
17
+ */
18
+ import { buildToolSemanticRegistry } from '@principles/core/runtime-v2';
19
+ export const CODEX_TOOL_SEMANTIC_MAPPINGS = Object.freeze([
20
+ { rawToolName: 'Bash', canonicalKind: 'execute' },
21
+ { rawToolName: 'apply_patch', canonicalKind: 'write' },
22
+ ]);
23
+ const built = buildToolSemanticRegistry(CODEX_TOOL_SEMANTIC_MAPPINGS);
24
+ if (!built.ok) {
25
+ throw new Error(`[PD] Codex tool semantic declaration is invalid: ${built.errors.join('; ')}`);
26
+ }
27
+ export const CODEX_TOOL_SEMANTICS = built.registry;
@@ -0,0 +1,59 @@
1
+ import { type InternalizationConsumerCycleOutcome, type ConsumerCycleLogger, type ReconcileGovernanceContinuationResult } from '@principles/host-runtime';
2
+ import { type CodexCatchUpResult } from '../ingestion/catch-up.js';
3
+ import type { TranscriptPort } from '../ingestion/transcript-decoder.js';
4
+ export type CodexWorkerMode = 'ready' | 'manual_action_required' | 'paused' | 'degraded';
5
+ export interface CodexWorkerCycleStepReport {
6
+ readonly catchUp: CodexCatchUpResult;
7
+ readonly reconcile: ReconcileGovernanceContinuationResult;
8
+ /** At most ONE diagnostician execution per cycle (bounded work). */
9
+ readonly diagnostician: {
10
+ readonly taskId: string;
11
+ readonly status: 'succeeded' | 'failed' | 'retried' | 'skipped' | 'degraded';
12
+ readonly message?: string;
13
+ readonly errorCategory?: string;
14
+ /** PRI-638 P1-C: recovery action for capability-disabled pauses. */
15
+ readonly nextAction?: string;
16
+ } | null;
17
+ readonly downstream: InternalizationConsumerCycleOutcome | null;
18
+ }
19
+ export interface CodexWorkerCycleResult {
20
+ readonly workspaceDir: string;
21
+ readonly mode: CodexWorkerMode;
22
+ readonly reason?: string;
23
+ readonly nextAction?: string;
24
+ readonly report?: CodexWorkerCycleStepReport;
25
+ }
26
+ export interface CodexWorkerCycleOptions {
27
+ readonly workspaceDir: string;
28
+ readonly env?: {
29
+ CODEX_HOME?: string | undefined;
30
+ };
31
+ readonly now?: Date;
32
+ readonly logger?: ConsumerCycleLogger;
33
+ readonly emitEvent?: (event: string, payloadJson: string) => void;
34
+ readonly port?: TranscriptPort;
35
+ /** Cap on diagnostician candidates inspected per cycle (default 5). */
36
+ readonly diagnosticianCandidateLimit?: number;
37
+ }
38
+ /**
39
+ * Run ONE bounded worker cycle for a canonical workspace. Never throws:
40
+ * every failure becomes a structured degraded result with a nextAction.
41
+ */
42
+ export declare function runCodexWorkspaceWorkerCycle(options: CodexWorkerCycleOptions): Promise<CodexWorkerCycleResult>;
43
+ export interface CodexWorkerStatusEvaluation {
44
+ readonly mode: CodexWorkerMode;
45
+ readonly reason?: string;
46
+ readonly nextAction?: string;
47
+ }
48
+ /**
49
+ * SPEC §15 worker mode, evaluated WITHOUT executing anything (no lease, no
50
+ * LLM, no transcript I/O). `manual_action_required` means no
51
+ * Companion-registered worker serves this workspace — the manual CLI path
52
+ * (catch-up / diagnose / run-once) is the recovery route. 'ready' here means
53
+ * "an automatic worker would run and hold the workspace task leases";
54
+ * live-worker liveness surfacing belongs to the Slice D health surface.
55
+ */
56
+ export declare function computeCodexWorkerStatusMode(input: {
57
+ workspaceDir: string;
58
+ registeredInInstallManifest: boolean;
59
+ }): CodexWorkerStatusEvaluation;
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Codex Workspace worker cycle (PRI-624 Slice C; SPEC §13/§15; ADR-0020 §11.1).
3
+ *
4
+ * ONE Workspace-scoped worker cycle with exactly three background
5
+ * responsibilities (the Owner-approved MVP exception — NOT a general daemon):
6
+ *
7
+ * 1. catch up transcript lag (gated by codex_conversation_ingestion)
8
+ * 2. run the Slice B idempotent reconciliation pass
9
+ * 3. lease + run one Diagnostician task, then ONE bounded downstream
10
+ * consumer cycle through the SHARED host-neutral executor
11
+ * (internalization_auto_consumer = workspace execution authority)
12
+ *
13
+ * Every step reuses existing authority: catch-up reuses the Slice A/B
14
+ * ingestion + admission seams, reconciliation reuses
15
+ * `reconcileGovernanceContinuation`, the diagnostician reuses the
16
+ * PainSignalBridge lease/runner contract, downstream reuses the same
17
+ * `runInternalizationConsumerCycle` the OpenClaw auto-consumer runs. No new
18
+ * task store, no private retry queue, no second scheduler state.
19
+ *
20
+ * Correctness under concurrent consumers (OpenClaw auto-consumer + this
21
+ * worker on one workspace) is owned by the durable Runtime V2 task lease —
22
+ * never by process-local state.
23
+ */
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { computeFeatureFlagsFromConfig, createRuntimeStateHandle, createPainSignalBridge, isRetryWaitBackoffElapsed, PrincipleTreeLedgerAdapter, resolveDiagnosticianCapability, } from '@principles/core/runtime-v2';
27
+ import { loadPdConfigForPlugin, loadFeatureFlagFromConfig, reconcileGovernanceContinuation, runInternalizationConsumerCycle, saveHostToolDeclaration, } from '@principles/host-runtime';
28
+ import { catchUpCodexIngestion } from '../ingestion/catch-up.js';
29
+ import { CODEX_TOOL_SEMANTIC_MAPPINGS, CODEX_TOOL_SEMANTICS } from '../tool-semantics.js';
30
+ const WORKER_OWNER = 'companion-worker';
31
+ const DEFAULT_DIAG_CANDIDATE_LIMIT = 5;
32
+ function workerLogger(logger) {
33
+ return logger ?? {
34
+ info: () => undefined,
35
+ warn: () => undefined,
36
+ error: () => undefined,
37
+ };
38
+ }
39
+ function directoryExists(dir) {
40
+ try {
41
+ return fs.statSync(dir).isDirectory();
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ /**
48
+ * Oldest pending diagnostician first; retry_wait candidates are filtered to
49
+ * those whose backoff window has ELAPSED, so an old-but-still-waiting task
50
+ * cannot starve a younger eligible one (review P1). The candidate set stays
51
+ * bounded (`limit`), and executePendingDiagnosis re-enforces the backoff
52
+ * window on the chosen task — no double bookkeeping.
53
+ */
54
+ async function pickDiagnosticianCandidate(stateManager, limit) {
55
+ const pending = await stateManager.listTasks({ taskKind: 'diagnostician', status: 'pending', orderBy: 'updated_at_asc', limit });
56
+ const [first] = pending;
57
+ if (first !== undefined)
58
+ return first;
59
+ const retrying = await stateManager.listTasks({ taskKind: 'diagnostician', status: 'retry_wait', orderBy: 'updated_at_asc', limit });
60
+ for (const candidate of retrying) {
61
+ if (isRetryWaitBackoffElapsed(candidate.status, candidate.leaseExpiresAt)) {
62
+ return candidate;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * Run ONE bounded worker cycle for a canonical workspace. Never throws:
69
+ * every failure becomes a structured degraded result with a nextAction.
70
+ */
71
+ export async function runCodexWorkspaceWorkerCycle(options) {
72
+ const workspaceDir = path.resolve(options.workspaceDir);
73
+ const logger = workerLogger(options.logger);
74
+ const emitEvent = options.emitEvent ?? ((_event, _payload) => undefined);
75
+ const base = { workspaceDir };
76
+ // Step 1 — Workspace eligibility.
77
+ if (!directoryExists(workspaceDir)) {
78
+ return { ...base, mode: 'degraded', reason: 'workspace_missing', nextAction: 'The workspace directory no longer exists; remove it from the install manifest or restore it. No PD state was mutated.' };
79
+ }
80
+ const config = loadPdConfigForPlugin(workspaceDir);
81
+ if (!config.ok) {
82
+ const [first] = config.errors;
83
+ return { ...base, mode: 'degraded', reason: `pd_config_invalid:${first?.reason ?? 'unknown'}`, nextAction: first?.nextAction ?? 'Repair .pd/config.yaml.' };
84
+ }
85
+ const { flags } = computeFeatureFlagsFromConfig(config.effective);
86
+ if (flags['host.codex']?.enabled !== true) {
87
+ 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.' };
88
+ }
89
+ const consumerFlag = loadFeatureFlagFromConfig(workspaceDir, 'internalization_auto_consumer', { info: (m) => logger.info(m), warn: (m) => logger.warn(m) });
90
+ // PRI-638 P1-C: the Diagnostician capability gate is resolved from the same
91
+ // canonical authority the runtime factory uses. An Owner-disabled agent is
92
+ // an intentional governance state: the worker must NOT pick pending tasks,
93
+ // must NOT invoke any runner (provider calls = 0), must NOT report the
94
+ // workspace degraded, and must keep catch-up / reconciliation / downstream
95
+ // running. Pending tasks stay untouched and resume on a later cycle after
96
+ // the Owner re-enables the agent.
97
+ const capability = resolveDiagnosticianCapability(config.effective);
98
+ const diagnosticianDisabled = !capability.available;
99
+ // Step 2 — Catch-up transcript lag (SPEC §13: gated by the ingestion flag,
100
+ // NOT by the consumer flag; catchUpCodexIngestion re-checks and returns a
101
+ // zero-I/O skip when ingestion is off).
102
+ const catchUp = await catchUpCodexIngestion({
103
+ workspaceDir,
104
+ env: options.env,
105
+ now: options.now,
106
+ port: options.port,
107
+ });
108
+ // Step 3 — Slice B idempotent reconciliation (always runs; creates no LLM work).
109
+ const reconcile = await reconcileGovernanceContinuation({ workspaceDir });
110
+ // Steps 4–6 — execution authority: internalization_auto_consumer.
111
+ let diagnostician = null;
112
+ if (diagnosticianDisabled) {
113
+ // PRI-638 P1-C: intentional governance pause. No candidate picking, no
114
+ // provider call, no degraded mode. The rest of the cycle (downstream)
115
+ // continues below; pending tasks resume once the Owner re-enables.
116
+ diagnostician = {
117
+ taskId: '',
118
+ status: 'skipped',
119
+ message: 'capability_disabled',
120
+ nextAction: capability.available ? undefined : capability.nextAction,
121
+ };
122
+ }
123
+ else if (!consumerFlag.enabled) {
124
+ // paused: execution pause, NOT evidence freeze. Catch-up + reconcile
125
+ // already ran above; manual CLI remains allowed (SPEC §13).
126
+ return {
127
+ ...base,
128
+ mode: 'paused',
129
+ reason: 'internalization_auto_consumer_disabled',
130
+ nextAction: 'Automatic internalization execution is paused for this Workspace; manual commands remain available: pd diagnose, pd runtime internalization run-once.',
131
+ report: { catchUp, reconcile, diagnostician: null, downstream: null },
132
+ };
133
+ }
134
+ else {
135
+ // Step 4 — expired-lease recovery sweep, then Step 5/6: at most one
136
+ // Diagnostician task via the existing bridge lease/runner contract.
137
+ // (Reached only when the Diagnostician capability is enabled AND the
138
+ // consumer flag is on — an intentional capability pause never reaches
139
+ // candidate picking or any provider call.)
140
+ try {
141
+ const handle = await createRuntimeStateHandle({ workspaceDir, readonly: false });
142
+ try {
143
+ const sweep = await handle.stateManager.runRecoverySweep();
144
+ if (sweep.recovered > 0 || sweep.errors.length > 0) {
145
+ emitEvent('CODEX_WORKER_RECOVERY_SWEEP', JSON.stringify({ recovered: sweep.recovered, failed: sweep.errors.length }));
146
+ }
147
+ const candidate = await pickDiagnosticianCandidate(handle.stateManager, options.diagnosticianCandidateLimit ?? DEFAULT_DIAG_CANDIDATE_LIMIT);
148
+ if (candidate !== null) {
149
+ const stateDir = path.join(workspaceDir, '.state');
150
+ const bridge = await createPainSignalBridge({
151
+ workspaceDir,
152
+ stateDir,
153
+ ledgerAdapter: new PrincipleTreeLedgerAdapter({ stateDir }),
154
+ owner: WORKER_OWNER,
155
+ effectiveConfig: config.effective,
156
+ getEnvVar: (name) => process.env[name],
157
+ });
158
+ const executed = await bridge.executePendingDiagnosis({ taskId: candidate.taskId });
159
+ diagnostician = {
160
+ taskId: candidate.taskId,
161
+ status: executed.status,
162
+ ...(executed.message !== undefined ? { message: executed.message.slice(0, 200) } : {}),
163
+ ...(executed.errorCategory !== undefined ? { errorCategory: executed.errorCategory } : {}),
164
+ };
165
+ // The bridge stays cached per workspace (bounded: one per workspace
166
+ // per worker process, exactly like the OpenClaw plugin host) — no
167
+ // per-cycle dispose, so a concurrent cycle can never hit a disposed
168
+ // bridge. The factory self-disposes losing concurrent constructions.
169
+ }
170
+ else {
171
+ // No eligible candidate, but a retry_wait task may still be inside
172
+ // its backoff window — report it so the cycle is observable instead
173
+ // of looking like "nothing pending" (review P1: head-of-line).
174
+ const waiting = await handle.stateManager.listTasks({ taskKind: 'diagnostician', status: 'retry_wait', orderBy: 'updated_at_asc', limit: 1 });
175
+ const [oldestWaiting] = waiting;
176
+ if (oldestWaiting !== undefined && !isRetryWaitBackoffElapsed(oldestWaiting.status, oldestWaiting.leaseExpiresAt)) {
177
+ diagnostician = { taskId: oldestWaiting.taskId, status: 'skipped', message: 'retry_wait_pending' };
178
+ }
179
+ }
180
+ }
181
+ finally {
182
+ await handle.close().catch(() => undefined);
183
+ }
184
+ }
185
+ catch (error) {
186
+ const detail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
187
+ return {
188
+ ...base,
189
+ mode: 'degraded',
190
+ reason: `diagnostician_execution_failed:${detail}`,
191
+ nextAction: 'Inspect the Workspace runtime profile and provider configuration; the task keeps its pending/retry state and no evidence was mutated.',
192
+ report: { catchUp, reconcile, diagnostician: null, downstream: null },
193
+ };
194
+ }
195
+ }
196
+ // Step 7 — ONE bounded downstream consumer cycle via the shared executor
197
+ // (same implementation the OpenClaw auto-consumer runs).
198
+ // PRI-634-F R2: persist the Codex tool declaration (workspace provenance
199
+ // for host-neutral consumers) and thread the registry into the cycle so
200
+ // activation-gate replay resolves Codex tool semantics (Bash, not bash).
201
+ const declared = saveHostToolDeclaration(workspaceDir, {
202
+ version: 1,
203
+ hostKind: 'codex',
204
+ mappings: CODEX_TOOL_SEMANTIC_MAPPINGS,
205
+ declaredAt: new Date().toISOString(),
206
+ });
207
+ if (!declared.ok) {
208
+ logger.warn?.(`[PD:CodexWorker] Failed to persist Codex tool declaration: ${declared.reason} — host-neutral consumers will not find it (rc-9)`);
209
+ }
210
+ const downstream = await runInternalizationConsumerCycle(workspaceDir, {
211
+ owner: WORKER_OWNER,
212
+ logLabel: 'CodexWorker',
213
+ logger,
214
+ emitEvent,
215
+ toolSemantics: CODEX_TOOL_SEMANTICS,
216
+ // No hostToolCatalog: PD has not declared a Codex tool catalog; a wrong
217
+ // (OpenClaw) catalog would be worse than none (PRI-630 follow-up).
218
+ });
219
+ // Aggregated mode: degraded overrides all (review P1). Any component
220
+ // degrading — catch-up, reconciliation, diagnostician or downstream —
221
+ // surfaces the workspace as degraded, with the highest-priority reason.
222
+ // A lease_conflict diagnostician failure is contention, not degradation:
223
+ // another consumer owns the task, which is normal operation.
224
+ const degradedReason = catchUp.status === 'degraded' ? `catch_up:${catchUp.remainingLagRollouts[0] ?? 'unknown'}`
225
+ : !reconcile.ok ? `reconcile:${reconcile.reason ?? 'failed'}`
226
+ : diagnostician?.status === 'failed' && diagnostician.errorCategory !== 'lease_conflict' ? `diagnostician_failed:${diagnostician.message ?? 'max_attempts_exceeded'}`
227
+ : diagnostician?.status === 'degraded' ? `diagnostician_degraded:${diagnostician.message ?? 'unknown'}`
228
+ : downstream.skipReason === 'runtime_config_error' || downstream.skipReason === 'cycle_error' || downstream.skipReason === 'config_malformed' ? `downstream:${downstream.skipReason}`
229
+ : undefined;
230
+ return {
231
+ ...base,
232
+ mode: degradedReason !== undefined ? 'degraded' : 'ready',
233
+ ...(degradedReason !== undefined ? { reason: degradedReason, nextAction: 'Inspect the Workspace .pd/config.yaml runtime profile and the per-step report above; the worker retries automatically on the next cycle.' } : {}),
234
+ report: { catchUp, reconcile, diagnostician, downstream },
235
+ };
236
+ }
237
+ /**
238
+ * SPEC §15 worker mode, evaluated WITHOUT executing anything (no lease, no
239
+ * LLM, no transcript I/O). `manual_action_required` means no
240
+ * Companion-registered worker serves this workspace — the manual CLI path
241
+ * (catch-up / diagnose / run-once) is the recovery route. 'ready' here means
242
+ * "an automatic worker would run and hold the workspace task leases";
243
+ * live-worker liveness surfacing belongs to the Slice D health surface.
244
+ */
245
+ export function computeCodexWorkerStatusMode(input) {
246
+ const workspaceDir = path.resolve(input.workspaceDir);
247
+ if (!directoryExists(workspaceDir)) {
248
+ return { mode: 'degraded', reason: 'workspace_missing', nextAction: 'The workspace directory does not exist; restore it or remove it from the install manifest.' };
249
+ }
250
+ const config = loadPdConfigForPlugin(workspaceDir);
251
+ if (!config.ok) {
252
+ const [first] = config.errors;
253
+ return { mode: 'degraded', reason: `pd_config_invalid:${first?.reason ?? 'unknown'}`, nextAction: first?.nextAction ?? 'Repair .pd/config.yaml.' };
254
+ }
255
+ const { flags } = computeFeatureFlagsFromConfig(config.effective);
256
+ if (flags['host.codex']?.enabled !== true) {
257
+ return { mode: 'paused', reason: 'host.codex_disabled', nextAction: 'Set features.host.codex.enabled=true in the Workspace .pd/config.yaml to enable Codex PD behavior.' };
258
+ }
259
+ if (flags.internalization_auto_consumer?.enabled !== true) {
260
+ return { mode: 'paused', reason: 'internalization_auto_consumer_disabled', nextAction: 'Automatic execution is paused; manual commands remain available: pd diagnose, pd runtime internalization run-once.' };
261
+ }
262
+ if (!input.registeredInInstallManifest) {
263
+ return {
264
+ mode: 'manual_action_required',
265
+ reason: 'workspace_not_in_install_manifest',
266
+ nextAction: `No Companion worker is registered for this workspace. Manual path: pd codex ingest catch-up --workspace "${workspaceDir}", then pd diagnose / pd runtime internalization run-once.`,
267
+ };
268
+ }
269
+ return { mode: 'ready' };
270
+ }