@principles/pd-cli 1.133.0 → 1.134.0

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 (211) hide show
  1. package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
  2. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
  3. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
  4. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
  5. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
  6. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
  7. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
  8. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
  9. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
  10. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
  11. package/dist/commands/admission-gate.d.ts +24 -0
  12. package/dist/commands/admission-gate.d.ts.map +1 -0
  13. package/dist/commands/admission-gate.js +29 -0
  14. package/dist/commands/admission-gate.js.map +1 -0
  15. package/dist/commands/candidate.d.ts.map +1 -1
  16. package/dist/commands/candidate.js +72 -6
  17. package/dist/commands/candidate.js.map +1 -1
  18. package/dist/commands/console.d.ts.map +1 -1
  19. package/dist/commands/console.js +40 -0
  20. package/dist/commands/console.js.map +1 -1
  21. package/dist/commands/demo-story-a.d.ts.map +1 -1
  22. package/dist/commands/demo-story-a.js +4 -2
  23. package/dist/commands/demo-story-a.js.map +1 -1
  24. package/dist/commands/diagnose.d.ts.map +1 -1
  25. package/dist/commands/diagnose.js +155 -8
  26. package/dist/commands/diagnose.js.map +1 -1
  27. package/dist/commands/errors-list.d.ts +25 -0
  28. package/dist/commands/errors-list.d.ts.map +1 -0
  29. package/dist/commands/errors-list.js +368 -0
  30. package/dist/commands/errors-list.js.map +1 -0
  31. package/dist/commands/health-codex.d.ts +7 -0
  32. package/dist/commands/health-codex.d.ts.map +1 -0
  33. package/dist/commands/health-codex.js +209 -0
  34. package/dist/commands/health-codex.js.map +1 -0
  35. package/dist/commands/health.d.ts +24 -0
  36. package/dist/commands/health.d.ts.map +1 -1
  37. package/dist/commands/health.js +48 -0
  38. package/dist/commands/health.js.map +1 -1
  39. package/dist/commands/intent.d.ts +21 -4
  40. package/dist/commands/intent.d.ts.map +1 -1
  41. package/dist/commands/intent.js +88 -27
  42. package/dist/commands/intent.js.map +1 -1
  43. package/dist/commands/pain-evidence.d.ts +6 -0
  44. package/dist/commands/pain-evidence.d.ts.map +1 -1
  45. package/dist/commands/pain-evidence.js +6 -0
  46. package/dist/commands/pain-evidence.js.map +1 -1
  47. package/dist/commands/pain-record.d.ts.map +1 -1
  48. package/dist/commands/pain-record.js +21 -0
  49. package/dist/commands/pain-record.js.map +1 -1
  50. package/dist/commands/pain-retry.d.ts +4 -0
  51. package/dist/commands/pain-retry.d.ts.map +1 -1
  52. package/dist/commands/pain-retry.js +327 -32
  53. package/dist/commands/pain-retry.js.map +1 -1
  54. package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
  55. package/dist/commands/proven-channel-baseline.js +4 -2
  56. package/dist/commands/proven-channel-baseline.js.map +1 -1
  57. package/dist/commands/runtime-activation.d.ts +30 -0
  58. package/dist/commands/runtime-activation.d.ts.map +1 -1
  59. package/dist/commands/runtime-activation.js +567 -11
  60. package/dist/commands/runtime-activation.js.map +1 -1
  61. package/dist/commands/runtime-canary.d.ts.map +1 -1
  62. package/dist/commands/runtime-canary.js +32 -7
  63. package/dist/commands/runtime-canary.js.map +1 -1
  64. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
  65. package/dist/commands/runtime-health-snapshot.js +4 -2
  66. package/dist/commands/runtime-health-snapshot.js.map +1 -1
  67. package/dist/commands/runtime-init.d.ts +43 -0
  68. package/dist/commands/runtime-init.d.ts.map +1 -0
  69. package/dist/commands/runtime-init.js +264 -0
  70. package/dist/commands/runtime-init.js.map +1 -0
  71. package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
  72. package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
  73. package/dist/commands/runtime-internalization-context-trace.js +374 -0
  74. package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
  75. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  76. package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
  77. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  78. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
  79. package/dist/commands/runtime-internalization-integrity.js +4 -2
  80. package/dist/commands/runtime-internalization-integrity.js.map +1 -1
  81. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  82. package/dist/commands/runtime-internalization-run-once.js +20 -5
  83. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  84. package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
  85. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  86. package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
  87. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  88. package/dist/commands/runtime-uat.d.ts.map +1 -1
  89. package/dist/commands/runtime-uat.guard.test.js +13 -15
  90. package/dist/commands/runtime-uat.guard.test.js.map +1 -1
  91. package/dist/commands/runtime-uat.js +4 -3
  92. package/dist/commands/runtime-uat.js.map +1 -1
  93. package/dist/config-reader.js +1 -1
  94. package/dist/config-reader.js.map +1 -1
  95. package/dist/index.js +54 -78
  96. package/dist/index.js.map +1 -1
  97. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
  98. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
  99. package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
  100. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
  101. package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
  102. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
  103. package/dist/services/config-doctor.d.ts.map +1 -1
  104. package/dist/services/config-doctor.js +9 -2
  105. package/dist/services/config-doctor.js.map +1 -1
  106. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
  107. package/dist/services/pain-flood-simulation-runner.js +9 -0
  108. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  109. package/dist/services/pd-config-loader.js +2 -2
  110. package/dist/services/pd-config-loader.js.map +1 -1
  111. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
  112. package/dist/services/proven-channel-baseline-runner.js +10 -2
  113. package/dist/services/proven-channel-baseline-runner.js.map +1 -1
  114. package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
  115. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  116. package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
  117. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  118. package/dist/services/rulehost-pipeline-runner.js +111 -7
  119. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  120. package/dist/services/rulehost-readiness.js.map +1 -1
  121. package/dist/services/runtime-adapter-resolver.d.ts +1 -0
  122. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  123. package/dist/services/runtime-adapter-resolver.js +3 -0
  124. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  125. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  126. package/dist/services/synthetic-baseline-runner.js +11 -0
  127. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  128. package/dist/utils/production-workspace-guard.d.ts +4 -4
  129. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  130. package/dist/utils/production-workspace-guard.js +19 -16
  131. package/dist/utils/production-workspace-guard.js.map +1 -1
  132. package/dist/utils/production-workspace-guard.test.js +54 -19
  133. package/dist/utils/production-workspace-guard.test.js.map +1 -1
  134. package/package.json +8 -5
  135. package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
  136. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
  137. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
  138. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
  139. package/src/commands/admission-gate.ts +30 -0
  140. package/src/commands/candidate.ts +74 -6
  141. package/src/commands/console.ts +40 -0
  142. package/src/commands/demo-story-a.ts +4 -2
  143. package/src/commands/diagnose.ts +158 -10
  144. package/src/commands/errors-list.ts +440 -0
  145. package/src/commands/health-codex.ts +249 -0
  146. package/src/commands/health.ts +60 -0
  147. package/src/commands/intent.ts +98 -29
  148. package/src/commands/pain-evidence.ts +6 -0
  149. package/src/commands/pain-record.ts +21 -0
  150. package/src/commands/pain-retry.ts +353 -35
  151. package/src/commands/proven-channel-baseline.ts +4 -2
  152. package/src/commands/runtime-activation.ts +657 -12
  153. package/src/commands/runtime-canary.ts +31 -5
  154. package/src/commands/runtime-health-snapshot.ts +4 -2
  155. package/src/commands/runtime-init.ts +305 -0
  156. package/src/commands/runtime-internalization-context-trace.ts +445 -0
  157. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
  158. package/src/commands/runtime-internalization-integrity.ts +4 -2
  159. package/src/commands/runtime-internalization-run-once.ts +20 -5
  160. package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
  161. package/src/commands/runtime-uat.guard.test.ts +14 -16
  162. package/src/commands/runtime-uat.ts +4 -3
  163. package/src/config-reader.ts +1 -1
  164. package/src/index.ts +69 -78
  165. package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
  166. package/src/services/config-doctor.ts +9 -2
  167. package/src/services/pain-flood-simulation-runner.ts +9 -0
  168. package/src/services/pd-config-loader.ts +2 -2
  169. package/src/services/proven-channel-baseline-runner.ts +10 -3
  170. package/src/services/rulehost-pipeline-runner.ts +157 -6
  171. package/src/services/runtime-adapter-resolver.ts +4 -0
  172. package/src/services/synthetic-baseline-runner.ts +11 -0
  173. package/src/utils/production-workspace-guard.test.ts +62 -20
  174. package/src/utils/production-workspace-guard.ts +19 -16
  175. package/tests/bdd/cli-contract.steps.ts +343 -0
  176. package/tests/bdd/context-trace.steps.ts +229 -0
  177. package/tests/bdd/support/gherkin-loader.ts +147 -0
  178. package/tests/bdd/support/repo-root.ts +62 -0
  179. package/tests/bdd/support/vitest-bdd.ts +179 -0
  180. package/tests/commands/admission-gate-coverage.test.ts +79 -0
  181. package/tests/commands/candidate-audit-repair.test.ts +77 -0
  182. package/tests/commands/candidate-intake.test.ts +155 -10
  183. package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
  184. package/tests/commands/cli-help-snapshot.test.ts +15 -1
  185. package/tests/commands/console-open.test.ts +19 -0
  186. package/tests/commands/diagnose.test.ts +619 -14
  187. package/tests/commands/errors-list.test.ts +678 -0
  188. package/tests/commands/health-codex.test.ts +243 -0
  189. package/tests/commands/health-host-wiring.test.ts +76 -0
  190. package/tests/commands/intent.test.ts +4 -4
  191. package/tests/commands/pain-record.test.ts +80 -0
  192. package/tests/commands/pain-retry.test.ts +94 -3
  193. package/tests/commands/run-rulehost-handler.test.ts +133 -0
  194. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
  195. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
  196. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
  197. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
  198. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
  199. package/tests/commands/runtime-activation.test.ts +1090 -5
  200. package/tests/commands/runtime-canary.test.ts +50 -0
  201. package/tests/commands/runtime-features.test.ts +2 -0
  202. package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
  203. package/tests/commands/runtime-init.test.ts +346 -0
  204. package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
  205. package/tests/commands/runtime-internalization.test.ts +2 -2
  206. package/tests/config-reader.test.ts +6 -5
  207. package/tests/e2e/cross-package-acceptance.test.ts +55 -13
  208. package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
  209. package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
  210. package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
  211. package/vitest.config.ts +1 -1
@@ -0,0 +1,249 @@
1
+ /**
2
+ * pd health --host codex command implementation.
3
+ *
4
+ * Reports Codex adapter/runtime versions, host.codex feature flag state,
5
+ * hook trust status, and dual global/plugin hook registration detection.
6
+ *
7
+ * CLI gate compliance:
8
+ * - cli-1: --json outputs exactly one parseable JSON object on stdout.
9
+ * - cli-2: exit paths stop execution (return after process.exit).
10
+ * - cli-5: failure paths do not mutate state (read-only throughout).
11
+ * - cli-6: every degraded/refused result includes reason + nextAction.
12
+ */
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ import * as os from 'os';
16
+ import { createRequire } from 'module';
17
+ import { resolveWorkspaceDir } from '../resolve-workspace.js';
18
+ import { loadPdConfigForPlugin, resolveNearestPdWorkspace } from '@principles/host-runtime';
19
+ import { computeFeatureFlagsFromConfig, isFeatureEnabled } from '@principles/core/runtime-v2';
20
+
21
+ const require = createRequire(import.meta.url);
22
+
23
+ interface CodexHealthOptions {
24
+ workspace?: string;
25
+ json?: boolean;
26
+ }
27
+
28
+ interface CodexHealthReport {
29
+ generatedAt: string;
30
+ host: 'codex';
31
+ workspace: string;
32
+ adapterVersion: string;
33
+ runtimeVersion: string;
34
+ featureFlag: {
35
+ name: 'host.codex';
36
+ enabled: boolean;
37
+ source: 'user_config' | 'defaults' | 'malformed';
38
+ reason?: string;
39
+ nextAction?: string;
40
+ };
41
+ hooksTrust: {
42
+ detectable: boolean;
43
+ trusted?: boolean;
44
+ reason?: string;
45
+ nextAction?: string;
46
+ };
47
+ dualRegistration: {
48
+ detected: boolean;
49
+ globalHooksPath?: string;
50
+ pluginHooksPath?: string;
51
+ reason?: string;
52
+ nextAction?: string;
53
+ };
54
+ warnings: string[];
55
+ }
56
+
57
+ function readPackageVersion(packageName: string): string {
58
+ try {
59
+ const pkgJsonPath = require.resolve(`${packageName}/package.json`);
60
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) as { version?: unknown };
61
+ if (typeof pkg.version === 'string') return pkg.version;
62
+ return 'unknown';
63
+ } catch {
64
+ return 'unknown';
65
+ }
66
+ }
67
+
68
+ function detectCodexConfigDir(): string | undefined {
69
+ const home = os.homedir();
70
+ const codexDir = path.join(home, '.codex');
71
+ if (fs.existsSync(codexDir)) return codexDir;
72
+ return undefined;
73
+ }
74
+
75
+ function detectHooksTrust(codexConfigDir: string | undefined): CodexHealthReport['hooksTrust'] {
76
+ if (!codexConfigDir) {
77
+ return {
78
+ detectable: false,
79
+ reason: 'codex_config_dir_not_found',
80
+ nextAction: 'Install Codex CLI (>= 0.147) and run `codex` once to create ~/.codex/, then re-run `pd health --host codex` to verify hook trust state.',
81
+ };
82
+ }
83
+ // Codex stores hook trust in config.toml under [features] hooks = true|false.
84
+ // We do a best-effort text scan rather than a full TOML parse to avoid a new
85
+ // dependency. If the file is missing or the hook line is absent, we report
86
+ // undetectable with a clear next action.
87
+ const configTomlPath = path.join(codexConfigDir, 'config.toml');
88
+ if (!fs.existsSync(configTomlPath)) {
89
+ return {
90
+ detectable: false,
91
+ reason: 'config_toml_not_found',
92
+ nextAction: `Open Codex and run /hooks to trust PD hooks, then re-run \`pd health --host codex\`. Expected config at ${configTomlPath}.`,
93
+ };
94
+ }
95
+ try {
96
+ const raw = fs.readFileSync(configTomlPath, 'utf8');
97
+ const match = /hooks\s*=\s*(true|false)/i.exec(raw);
98
+ if (!match || !match[1]) {
99
+ return {
100
+ detectable: false,
101
+ reason: 'hooks_setting_not_found_in_config',
102
+ nextAction: 'Open Codex and run /hooks to trust PD hooks. Codex config.toml exists but has no `hooks` setting under [features].',
103
+ };
104
+ }
105
+ return { detectable: true, trusted: match[1].toLowerCase() === 'true' };
106
+ } catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ return {
109
+ detectable: false,
110
+ reason: `config_toml_read_failed: ${message.slice(0, 200)}`,
111
+ nextAction: `Check read permissions on ${configTomlPath} and re-run \`pd health --host codex\`.`,
112
+ };
113
+ }
114
+ }
115
+
116
+ function detectDualRegistration(codexConfigDir: string | undefined): CodexHealthReport['dualRegistration'] {
117
+ const globalHooksPath = codexConfigDir ? path.join(codexConfigDir, 'hooks.json') : undefined;
118
+ // Plugin hooks are declared in a plugin manifest; we cannot reliably detect
119
+ // them from outside Codex. We only flag the global-hooks path and let the
120
+ // operator confirm plugin registration via /hooks in Codex.
121
+ const globalHooksExists = globalHooksPath ? fs.existsSync(globalHooksPath) : false;
122
+ if (globalHooksExists) {
123
+ return {
124
+ detected: true,
125
+ globalHooksPath,
126
+ reason: 'global_hooks_json_present',
127
+ nextAction: 'Global ~/.codex/hooks.json is installed. If the PD Codex plugin is also installed via marketplace, this may cause double-registration. Choose ONE: keep global hooks.json (fallback path) OR uninstall it and use the plugin (recommended). To remove global PD entries, run `create-principles-disciple uninstall --host codex`.',
128
+ };
129
+ }
130
+ return { detected: false };
131
+ }
132
+
133
+ export async function handleHealthCodex(opts: CodexHealthOptions = {}): Promise<void> {
134
+ const generatedAt = new Date().toISOString();
135
+ const workspaceDir = opts.workspace
136
+ ? path.resolve(opts.workspace)
137
+ : resolveWorkspaceDir();
138
+
139
+ const warnings: string[] = [];
140
+
141
+ // Resolve workspace via the shared host-runtime resolver so pd-cli reports
142
+ // the same workspace the Codex hook would resolve.
143
+ const resolution = resolveNearestPdWorkspace(workspaceDir);
144
+ let resolvedWorkspace: string;
145
+ let featureFlagSource: 'user_config' | 'defaults' | 'malformed';
146
+ let hostCodexEnabled = false;
147
+
148
+ if (!resolution.ok) {
149
+ resolvedWorkspace = workspaceDir;
150
+ featureFlagSource = 'defaults';
151
+ warnings.push(`workspace_not_resolved: ${resolution.reason} — ${resolution.nextAction}`);
152
+ } else {
153
+ resolvedWorkspace = resolution.workspaceDir;
154
+ const configLoad = loadPdConfigForPlugin(resolvedWorkspace);
155
+ featureFlagSource = configLoad.source;
156
+ const flags = computeFeatureFlagsFromConfig(configLoad.effective);
157
+ hostCodexEnabled = isFeatureEnabled(flags, 'host.codex');
158
+ if (!configLoad.ok) {
159
+ for (const error of configLoad.errors) {
160
+ warnings.push(`config_error: ${error.reason} — ${error.nextAction}`);
161
+ }
162
+ }
163
+ }
164
+
165
+ const adapterVersion = readPackageVersion('@principles/codex-adapter');
166
+ const runtimeVersion = readPackageVersion('@principles/host-runtime');
167
+ const codexConfigDir = detectCodexConfigDir();
168
+ const hooksTrust = detectHooksTrust(codexConfigDir);
169
+ const dualRegistration = detectDualRegistration(codexConfigDir);
170
+
171
+ if (hooksTrust.reason) warnings.push(`hooks_trust: ${hooksTrust.reason}`);
172
+ if (dualRegistration.reason) warnings.push(`dual_registration: ${dualRegistration.reason}`);
173
+
174
+ const report: CodexHealthReport = {
175
+ generatedAt,
176
+ host: 'codex',
177
+ workspace: resolvedWorkspace,
178
+ adapterVersion,
179
+ runtimeVersion,
180
+ featureFlag: {
181
+ name: 'host.codex',
182
+ enabled: hostCodexEnabled,
183
+ source: featureFlagSource,
184
+ ...(hostCodexEnabled
185
+ ? {}
186
+ : {
187
+ reason: 'host_codex_disabled',
188
+ nextAction: `Enable the Codex host adapter by setting features.host.codex.enabled=true in ${resolvedWorkspace}/.pd/config.yaml, then re-run \`pd health --host codex\`.`,
189
+ }),
190
+ },
191
+ hooksTrust,
192
+ dualRegistration,
193
+ warnings,
194
+ };
195
+
196
+ if (opts.json) {
197
+ // cli-1: exactly one parseable JSON object on stdout.
198
+ console.log(JSON.stringify(report, null, 2));
199
+ // cli-2: exit-stops. Non-zero only when host.codex is disabled AND hooks
200
+ // are not trusted — the operator must act before Codex activation works.
201
+ if (!hostCodexEnabled && !hooksTrust.trusted) {
202
+ process.exitCode = 1;
203
+ }
204
+ return;
205
+ }
206
+
207
+ // Text output — still includes explicit nextAction per cli-6.
208
+ console.log(`generatedAt: ${report.generatedAt}`);
209
+ console.log(`host: ${report.host}`);
210
+ console.log(`workspace: ${report.workspace}`);
211
+ console.log(`adapterVersion: ${report.adapterVersion}`);
212
+ console.log(`runtimeVersion: ${report.runtimeVersion}`);
213
+ console.log(`featureFlag.name: ${report.featureFlag.name}`);
214
+ console.log(`featureFlag.enabled: ${report.featureFlag.enabled}`);
215
+ console.log(`featureFlag.source: ${report.featureFlag.source}`);
216
+ if (report.featureFlag.reason) {
217
+ console.log(`featureFlag.reason: ${report.featureFlag.reason}`);
218
+ console.log(`featureFlag.nextAction: ${report.featureFlag.nextAction ?? ''}`);
219
+ }
220
+ console.log(`hooksTrust.detectable: ${report.hooksTrust.detectable}`);
221
+ if (report.hooksTrust.trusted !== undefined) {
222
+ console.log(`hooksTrust.trusted: ${report.hooksTrust.trusted}`);
223
+ }
224
+ if (report.hooksTrust.reason) {
225
+ console.log(`hooksTrust.reason: ${report.hooksTrust.reason}`);
226
+ console.log(`hooksTrust.nextAction: ${report.hooksTrust.nextAction ?? ''}`);
227
+ }
228
+ console.log(`dualRegistration.detected: ${report.dualRegistration.detected}`);
229
+ if (report.dualRegistration.globalHooksPath) {
230
+ console.log(`dualRegistration.globalHooksPath: ${report.dualRegistration.globalHooksPath}`);
231
+ }
232
+ if (report.dualRegistration.reason) {
233
+ console.log(`dualRegistration.reason: ${report.dualRegistration.reason}`);
234
+ console.log(`dualRegistration.nextAction: ${report.dualRegistration.nextAction ?? ''}`);
235
+ }
236
+ if (report.warnings.length > 0) {
237
+ console.log(`warnings:`);
238
+ for (const w of report.warnings) console.log(` - ${w}`);
239
+ }
240
+ console.log('');
241
+
242
+ if (!hostCodexEnabled) {
243
+ console.warn(`⚠️ host.codex feature flag is disabled. Enable it in ${resolvedWorkspace}/.pd/config.yaml under features.host.codex.enabled to activate Codex hooks.`);
244
+ }
245
+ if (!hooksTrust.trusted && !hooksTrust.detectable) {
246
+ console.warn(`⚠️ Hook trust state could not be detected. ${hooksTrust.nextAction ?? ''}`);
247
+ process.exitCode = 1;
248
+ }
249
+ }
@@ -11,10 +11,12 @@
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
13
  import Database from 'better-sqlite3';
14
+ import type { Command } from 'commander';
14
15
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
15
16
  import { PruningReadModel, PainChainReadModel, auditCandidateLedgerConsistency } from '@principles/core/runtime-v2';
16
17
  import type { PainChainTrace } from '@principles/core/runtime-v2';
17
18
  import { getLedgerFilePathPublic } from '@principles/core/principle-tree-ledger';
19
+ import { handleHealthCodex } from './health-codex.js';
18
20
 
19
21
  interface LastSuccessfulChain {
20
22
  painId?: string;
@@ -188,3 +190,61 @@ export async function handleHealth(opts: HealthOptions = {}): Promise<void> {
188
190
 
189
191
  writeHealth();
190
192
  }
193
+
194
+ /**
195
+ * Handlers backing the `pd health` command. Injected so the registration
196
+ * wiring test can exercise the real `registerHealthCommand` without executing
197
+ * real workspace/DB I/O; production uses the defaults.
198
+ */
199
+ interface HealthCommandHandlers {
200
+ health?: typeof handleHealth;
201
+ healthCodex?: typeof handleHealthCodex;
202
+ }
203
+
204
+ /**
205
+ * Registers the `pd health` command on a Commander program.
206
+ *
207
+ * cli-7 test-wiring: this is the single registration used by both the CLI
208
+ * entrypoint and the wiring test, so a test exercising it covers the real
209
+ * production command (options + --host dispatch), not a copy.
210
+ *
211
+ * --host accepts only `openclaw` and `codex`; any other value is rejected with
212
+ * a structured reason/nextAction and a non-zero exit (cli-1/cli-2/cli-6). When
213
+ * `--json` is set, the rejection is emitted as exactly one parseable JSON
214
+ * object on stdout.
215
+ */
216
+ export function registerHealthCommand(program: Command, handlers: HealthCommandHandlers = {}): void {
217
+ const healthHandler = handlers.health ?? handleHealth;
218
+ const codexHandler = handlers.healthCodex ?? handleHealthCodex;
219
+ program
220
+ .command('health')
221
+ .description('Show health diagnostics for all workspaces')
222
+ .option('-w, --workspace <path>', 'Workspace directory')
223
+ .option('--json', 'Output raw JSON')
224
+ .option('--host <host>', 'Host to inspect (openclaw|codex). Defaults to openclaw workspace health.')
225
+ .action(async (opts) => {
226
+ const host = opts.host ?? 'openclaw';
227
+ if (host !== 'codex' && host !== 'openclaw') {
228
+ // cli-2: exit-stops — return after setting exitCode.
229
+ if (opts.json) {
230
+ console.log(JSON.stringify({
231
+ ok: false,
232
+ reason: 'invalid_host',
233
+ host,
234
+ nextAction: `Use a supported host: openclaw or codex.`,
235
+ }));
236
+ process.exitCode = 1;
237
+ return;
238
+ }
239
+ console.error(`Invalid --host "${host}". Supported hosts: openclaw, codex.`);
240
+ console.error(`nextAction: run \`pd health --host openclaw\` or \`pd health --host codex\`.`);
241
+ process.exitCode = 1;
242
+ return;
243
+ }
244
+ if (host === 'codex') {
245
+ await codexHandler(opts);
246
+ return;
247
+ }
248
+ await healthHandler(opts);
249
+ });
250
+ }
@@ -2,14 +2,17 @@
2
2
  * pd intent — Owner-authored INTENT.md management (PRI-466).
3
3
  *
4
4
  * Subcommands:
5
- * - init : create .principles/INTENT.md from the canonical template
6
- * - show : display a read-only summary of INTENT.md (sections, hash, warnings)
5
+ * - init : create .principles/INTENT.{lang}.md from the canonical template
6
+ * - show : display a read-only summary of INTENT.{lang}.md (sections, hash, warnings)
7
7
  *
8
8
  * `init` is not gated by the intent_engineering flag — the Owner can
9
9
  * initialise the intent doc at any time. `show` IS gated: flag-off returns
10
10
  * a structured `flag_disabled` result without touching the filesystem,
11
11
  * matching the Console backend contract.
12
12
  *
13
+ * Bilingual: --lang zh-CN|en (default zh-CN) selects the intent doc language.
14
+ * File naming: INTENT.zh-CN.md / INTENT.en.md via getIntentFilename(lang).
15
+ *
13
16
  * JSON mode is strict: --json outputs exactly one parseable JSON object on
14
17
  * stdout (CLI Operator Gate rule 1). Failure paths include structured
15
18
  * reason + nextAction (rule 6).
@@ -27,13 +30,14 @@ import * as path from 'node:path';
27
30
  import type { Command } from 'commander';
28
31
  import {
29
32
  INTENT_MAX_BYTES,
30
- INTENT_DOC_TEMPLATE,
33
+ getIntentFilename,
34
+ createIntentTemplate,
31
35
  parseIntentDocSections,
32
36
  computeIntentContentHash,
33
37
  validateIntentDocSections,
34
38
  isFeatureEnabled,
35
39
  } from '@principles/core/runtime-v2';
36
- import type { IntentDocSections, IntentDocWarning } from '@principles/core/runtime-v2';
40
+ import type { IntentDocSections, IntentDocWarning, IntentLang } from '@principles/core/runtime-v2';
37
41
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
38
42
  import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
39
43
  import { emitResult } from '../services/cli-output.js';
@@ -41,7 +45,23 @@ import { emitResult } from '../services/cli-output.js';
41
45
  // ── Constants ────────────────────────────────────────────────────────────────
42
46
 
43
47
  const INTENT_DIR = '.principles';
44
- const INTENT_FILENAME = 'INTENT.md';
48
+
49
+ /**
50
+ * Parse and validate the --lang option.
51
+ *
52
+ * Returns:
53
+ * - 'zh-CN' | 'en' when value is undefined (default zh-CN) or a valid lang.
54
+ * - null when value is a non-empty string that is neither 'zh-CN' nor 'en'.
55
+ *
56
+ * rc-9: invalid values MUST NOT silently fall back to zh-CN — that would
57
+ * let a typo like `--lang zh` quietly write to INTENT.zh-CN.md. Callers
58
+ * emit a structured `invalid_lang` error when null is returned.
59
+ */
60
+ export function parseLang(value: string | undefined): IntentLang | null {
61
+ if (value === undefined) return 'zh-CN';
62
+ if (value === 'zh-CN' || value === 'en') return value;
63
+ return null;
64
+ }
45
65
 
46
66
  // ── Output types ─────────────────────────────────────────────────────────────
47
67
 
@@ -54,7 +74,7 @@ export interface IntentInitOutput {
54
74
  }
55
75
 
56
76
  export interface IntentShowOutput {
57
- status: 'ok' | 'flag_disabled' | 'not_found' | 'oversized' | 'read_error';
77
+ status: 'ok' | 'skipped' | 'flag_disabled' | 'not_found' | 'oversized' | 'read_error';
58
78
  flagEnabled: boolean;
59
79
  found: boolean;
60
80
  path?: string;
@@ -68,8 +88,8 @@ export interface IntentShowOutput {
68
88
 
69
89
  // ── Helpers ──────────────────────────────────────────────────────────────────
70
90
 
71
- function getIntentFilePath(workspaceDir: string): string {
72
- return path.join(workspaceDir, INTENT_DIR, INTENT_FILENAME);
91
+ function getIntentFilePath(workspaceDir: string, lang: IntentLang): string {
92
+ return path.join(workspaceDir, INTENT_DIR, getIntentFilename(lang));
73
93
  }
74
94
 
75
95
  function sectionsToRecord(sections: IntentDocSections): Record<string, string> {
@@ -82,9 +102,9 @@ function sectionsToRecord(sections: IntentDocSections): Record<string, string> {
82
102
  return record;
83
103
  }
84
104
 
85
- function formatIntentShowText(o: IntentShowOutput): string {
105
+ function formatIntentShowText(o: IntentShowOutput, filename: string): string {
86
106
  const lines: string[] = [];
87
- lines.push(`INTENT.md — ${o.path}`);
107
+ lines.push(`${filename} — ${o.path}`);
88
108
  lines.push(`Content hash: ${o.contentHash}`);
89
109
  lines.push(`Last edited: ${o.lastEditedAt}`);
90
110
  lines.push('');
@@ -114,9 +134,29 @@ export interface IntentInitOptions {
114
134
  json?: boolean;
115
135
  dryRun?: boolean;
116
136
  confirm?: boolean;
137
+ lang?: string;
117
138
  }
118
139
 
119
140
  export async function handleIntentInit(opts: IntentInitOptions): Promise<void> {
141
+ const lang = parseLang(opts.lang);
142
+
143
+ // rc-9: invalid --lang must not silently fall back. Emit structured error.
144
+ if (lang === null) {
145
+ const output: IntentInitOutput = {
146
+ status: 'skipped',
147
+ path: '',
148
+ overwritten: false,
149
+ reason: 'invalid_lang',
150
+ nextAction: `--lang must be 'zh-CN' or 'en'. Received: ${opts.lang ?? ''}`,
151
+ };
152
+ emitResult(output, {
153
+ json: opts.json ?? false,
154
+ formatText: (o) => `Error: ${o.reason}\n→ ${o.nextAction}`,
155
+ });
156
+ process.exitCode = 1;
157
+ return;
158
+ }
159
+
120
160
  // CLI Gate rule 4: --dry-run and --confirm must be mutually exclusive.
121
161
  if (opts.dryRun && opts.confirm) {
122
162
  const output: IntentInitOutput = {
@@ -141,7 +181,7 @@ export async function handleIntentInit(opts: IntentInitOptions): Promise<void> {
141
181
  let dir: string;
142
182
  try {
143
183
  workspaceDir = resolveWorkspaceDir(opts.workspace);
144
- filePath = getIntentFilePath(workspaceDir);
184
+ filePath = getIntentFilePath(workspaceDir, lang);
145
185
  dir = path.dirname(filePath);
146
186
  } catch (err) {
147
187
  const reason = err instanceof Error ? err.message : String(err);
@@ -160,6 +200,8 @@ export async function handleIntentInit(opts: IntentInitOptions): Promise<void> {
160
200
  return;
161
201
  }
162
202
 
203
+ const filename = getIntentFilename(lang);
204
+
163
205
  // CLI Gate rule 4: state-mutating command defaults to dry-run unless --confirm.
164
206
  const isDryRun = opts.dryRun === true || opts.confirm !== true;
165
207
 
@@ -170,11 +212,11 @@ export async function handleIntentInit(opts: IntentInitOptions): Promise<void> {
170
212
  path: filePath,
171
213
  overwritten: false,
172
214
  reason: 'file_exists',
173
- nextAction: `Use --force to overwrite: pd intent init --force --confirm --workspace "${workspaceDir}"`,
215
+ nextAction: `Use --force to overwrite: pd intent init --force --confirm --lang ${lang} --workspace "${workspaceDir}"`,
174
216
  };
175
217
  emitResult(output, {
176
218
  json: opts.json ?? false,
177
- formatText: (o) => `INTENT.md already exists at ${o.path}\n→ ${o.nextAction}`,
219
+ formatText: (o) => `${filename} already exists at ${o.path}\n→ ${o.nextAction}`,
178
220
  });
179
221
  process.exitCode = 1;
180
222
  return;
@@ -186,17 +228,17 @@ export async function handleIntentInit(opts: IntentInitOptions): Promise<void> {
186
228
  path: filePath,
187
229
  overwritten: opts.force === true,
188
230
  reason: 'dry_run',
189
- nextAction: `Confirm write: pd intent init --confirm${opts.force ? ' --force' : ''} --workspace "${workspaceDir}"`,
231
+ nextAction: `Confirm write: pd intent init --confirm --lang ${lang}${opts.force ? ' --force' : ''} --workspace "${workspaceDir}"`,
190
232
  };
191
233
  emitResult(output, {
192
234
  json: opts.json ?? false,
193
- formatText: (o) => `[dry-run] Would create INTENT.md at ${o.path}${o.overwritten ? ' (overwritten)' : ''}\n→ ${o.nextAction}`,
235
+ formatText: (o) => `[dry-run] Would create ${filename} at ${o.path}${o.overwritten ? ' (overwritten)' : ''}\n→ ${o.nextAction}`,
194
236
  });
195
237
  return;
196
238
  }
197
239
 
198
240
  fs.mkdirSync(dir, { recursive: true });
199
- fs.writeFileSync(filePath, INTENT_DOC_TEMPLATE, 'utf8');
241
+ fs.writeFileSync(filePath, createIntentTemplate(lang), 'utf8');
200
242
 
201
243
  const output: IntentInitOutput = {
202
244
  status: 'ok',
@@ -205,7 +247,7 @@ export async function handleIntentInit(opts: IntentInitOptions): Promise<void> {
205
247
  };
206
248
  emitResult(output, {
207
249
  json: opts.json ?? false,
208
- formatText: (o) => `Created INTENT.md at ${o.path}${o.overwritten ? ' (overwritten)' : ''}\nNext: edit the file to declare your project intent, then run "pd intent show".`,
250
+ formatText: (o) => `Created ${filename} at ${o.path}${o.overwritten ? ' (overwritten)' : ''}\nNext: edit the file to declare your project intent, then run "pd intent show --lang ${lang}".`,
209
251
  });
210
252
  } catch (err) {
211
253
  const reason = err instanceof Error ? err.message : String(err);
@@ -228,9 +270,30 @@ export async function handleIntentInit(opts: IntentInitOptions): Promise<void> {
228
270
  export interface IntentShowOptions {
229
271
  workspace?: string;
230
272
  json?: boolean;
273
+ lang?: string;
231
274
  }
232
275
 
233
276
  export async function handleIntentShow(opts: IntentShowOptions): Promise<void> {
277
+ const lang = parseLang(opts.lang);
278
+
279
+ // rc-9: invalid --lang must not silently fall back. Emit structured error.
280
+ if (lang === null) {
281
+ const output: IntentShowOutput = {
282
+ status: 'skipped',
283
+ flagEnabled: false,
284
+ found: false,
285
+ warnings: [],
286
+ reason: 'invalid_lang',
287
+ nextAction: `--lang must be 'zh-CN' or 'en'. Received: ${opts.lang ?? ''}`,
288
+ };
289
+ emitResult(output, {
290
+ json: opts.json ?? false,
291
+ formatText: (o) => `Error: ${o.reason}\n→ ${o.nextAction}`,
292
+ });
293
+ process.exitCode = 1;
294
+ return;
295
+ }
296
+
234
297
  // CLI Gate rule 6: workspace resolution inside try/catch for structured errors.
235
298
  let workspaceDir: string;
236
299
  try {
@@ -258,6 +321,8 @@ export async function handleIntentShow(opts: IntentShowOptions): Promise<void> {
258
321
  const flagsResult = computeFlagsFromLoadResult(configResult);
259
322
  const flagEnabled = isFeatureEnabled(flagsResult, 'intent_engineering');
260
323
 
324
+ const filename = getIntentFilename(lang);
325
+
261
326
  if (!flagEnabled) {
262
327
  const output: IntentShowOutput = {
263
328
  status: 'flag_disabled',
@@ -265,7 +330,7 @@ export async function handleIntentShow(opts: IntentShowOptions): Promise<void> {
265
330
  found: false,
266
331
  warnings: [],
267
332
  reason: 'flag_disabled',
268
- nextAction: 'Enable the intent_engineering feature flag in .pd/config.yaml to read INTENT.md.',
333
+ nextAction: `Enable the intent_engineering feature flag in .pd/config.yaml to read ${filename}.`,
269
334
  };
270
335
  emitResult(output, {
271
336
  json: opts.json ?? false,
@@ -274,7 +339,7 @@ export async function handleIntentShow(opts: IntentShowOptions): Promise<void> {
274
339
  return;
275
340
  }
276
341
 
277
- const filePath = getIntentFilePath(workspaceDir);
342
+ const filePath = getIntentFilePath(workspaceDir, lang);
278
343
 
279
344
  try {
280
345
  if (!fs.existsSync(filePath)) {
@@ -284,11 +349,11 @@ export async function handleIntentShow(opts: IntentShowOptions): Promise<void> {
284
349
  found: false,
285
350
  warnings: [],
286
351
  reason: 'not_found',
287
- nextAction: `Create INTENT.md: pd intent init --workspace "${workspaceDir}"`,
352
+ nextAction: `Create ${filename}: pd intent init --confirm --lang ${lang} --workspace "${workspaceDir}"`,
288
353
  };
289
354
  emitResult(output, {
290
355
  json: opts.json ?? false,
291
- formatText: (o) => `INTENT.md not found.\n→ ${o.nextAction}`,
356
+ formatText: (o) => `${filename} not found.\n→ ${o.nextAction}`,
292
357
  });
293
358
  return;
294
359
  }
@@ -301,11 +366,11 @@ export async function handleIntentShow(opts: IntentShowOptions): Promise<void> {
301
366
  found: true,
302
367
  warnings: [],
303
368
  reason: 'oversized',
304
- nextAction: `INTENT.md exceeds ${INTENT_MAX_BYTES} bytes (${stat.size} bytes). Reduce content.`,
369
+ nextAction: `${filename} exceeds ${INTENT_MAX_BYTES} bytes (${stat.size} bytes). Reduce content.`,
305
370
  };
306
371
  emitResult(output, {
307
372
  json: opts.json ?? false,
308
- formatText: (o) => `INTENT.md is too large.\n→ ${o.nextAction}`,
373
+ formatText: (o) => `${filename} is too large.\n→ ${o.nextAction}`,
309
374
  });
310
375
  return;
311
376
  }
@@ -327,7 +392,7 @@ export async function handleIntentShow(opts: IntentShowOptions): Promise<void> {
327
392
  };
328
393
  emitResult(output, {
329
394
  json: opts.json ?? false,
330
- formatText: (o) => formatIntentShowText(o),
395
+ formatText: (o) => formatIntentShowText(o, filename),
331
396
  });
332
397
  } catch (err) {
333
398
  const reason = err instanceof Error ? err.message : String(err);
@@ -341,7 +406,7 @@ export async function handleIntentShow(opts: IntentShowOptions): Promise<void> {
341
406
  };
342
407
  emitResult(output, {
343
408
  json: opts.json ?? false,
344
- formatText: (o) => `Error reading INTENT.md: ${o.reason}\n→ ${o.nextAction}`,
409
+ formatText: (o) => `Error reading ${filename}: ${o.reason}\n→ ${o.nextAction}`,
345
410
  });
346
411
  process.exitCode = 1;
347
412
  }
@@ -356,11 +421,12 @@ export function registerIntentCommand(parentCmd: Command): Command {
356
421
 
357
422
  intentCmd
358
423
  .command('init')
359
- .description('Create .principles/INTENT.md from the canonical template')
424
+ .description('Create .principles/INTENT.{lang}.md from the canonical template')
360
425
  .option('-w, --workspace <path>', 'Workspace directory')
361
- .option('--force', 'Overwrite existing INTENT.md')
426
+ .option('--force', 'Overwrite existing INTENT file')
362
427
  .option('--dry-run', 'Show what would happen without writing (default)')
363
- .option('--confirm', 'Actually write the file (required to create INTENT.md)')
428
+ .option('--confirm', 'Actually write the file (required to create INTENT file)')
429
+ .option('--lang <lang>', 'Language: zh-CN or en (default: zh-CN)')
364
430
  .option('--json', 'Output raw JSON')
365
431
  .action(async (opts) => {
366
432
  await handleIntentInit({
@@ -369,18 +435,21 @@ export function registerIntentCommand(parentCmd: Command): Command {
369
435
  json: opts.json === true,
370
436
  dryRun: opts.dryRun === true,
371
437
  confirm: opts.confirm === true,
438
+ lang: opts.lang,
372
439
  });
373
440
  });
374
441
 
375
442
  intentCmd
376
443
  .command('show')
377
- .description('Display a read-only summary of INTENT.md (sections, hash, warnings)')
444
+ .description('Display a read-only summary of INTENT.{lang}.md (sections, hash, warnings)')
378
445
  .option('-w, --workspace <path>', 'Workspace directory')
446
+ .option('--lang <lang>', 'Language: zh-CN or en (default: zh-CN)')
379
447
  .option('--json', 'Output raw JSON')
380
448
  .action(async (opts) => {
381
449
  await handleIntentShow({
382
450
  workspace: opts.workspace,
383
451
  json: opts.json === true,
452
+ lang: opts.lang,
384
453
  });
385
454
  });
386
455
 
@@ -8,6 +8,12 @@
8
8
  * pd pain evidence [--workspace <path>] [--limit N] [--json]
9
9
  *
10
10
  * Shows the most recent TRIGGER_DECISION log entries.
11
+ *
12
+ * Note: TRIGGER_DECISION entries are pain-signal admission gate decisions
13
+ * (Gate B / TriggerController). These are distinct from GFI (General
14
+ * Friction Index) friction events like GFI_SLICE_RESET, which track
15
+ * session-level tool-friction resets and are NOT pain signals. This command
16
+ * does not surface GFI events; use `pd runtime health gfi` for GFI state.
11
17
  */
12
18
 
13
19
  import * as fs from 'fs';