create-principles-disciple 1.133.0 → 1.133.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/codex-adapter/dist/ingestion/ingestion.js +3 -3
- package/codex-adapter/dist/ingestion/transcript-locate.d.ts +6 -11
- package/codex-adapter/dist/ingestion/transcript-locate.js +5 -73
- package/codex-adapter/dist/ingestion/transcript-path.d.ts +2 -2
- package/codex-adapter/dist/ingestion/transcript-path.js +4 -21
- package/codex-adapter/dist/worker/workspace-worker.d.ts +1 -12
- package/codex-adapter/dist/worker/workspace-worker.js +4 -34
- package/codex-adapter/package.json +1 -1
- package/console/dist/server/models/CodexGovernanceHealthModel.d.ts +97 -0
- package/console/dist/server/models/CodexGovernanceHealthModel.js +81 -0
- package/console/dist/server/models/EvidenceChainConsoleModel.js +16 -2
- package/console/dist/server/routes/health.js +59 -3
- package/console/dist/ui/i18n/en.json +4 -0
- package/console/dist/ui/i18n/zh-CN.json +4 -0
- package/console/dist/ui/pages/pain/PainPage.js +1 -1
- package/console/dist/ui/utils/validators.d.ts +2 -0
- package/console/dist/ui/utils/validators.js +7 -0
- package/console/dist/web/assets/app.js +15 -1
- package/core/dist/host/index.d.ts +2 -0
- package/core/dist/host/index.d.ts.map +1 -1
- package/core/dist/host/index.js +3 -0
- package/core/dist/host/index.js.map +1 -1
- package/core/dist/host/legacy-registration.d.ts +28 -0
- package/core/dist/host/legacy-registration.d.ts.map +1 -0
- package/core/dist/host/legacy-registration.js +49 -0
- package/core/dist/host/legacy-registration.js.map +1 -0
- package/core/dist/runtime-v2/__tests__/evidence-chain-host-kind.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/evidence-chain-host-kind.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/evidence-chain-host-kind.test.js +49 -0
- package/core/dist/runtime-v2/__tests__/evidence-chain-host-kind.test.js.map +1 -0
- package/core/dist/runtime-v2/types/evidence-chain-contract.d.ts +6 -0
- package/core/dist/runtime-v2/types/evidence-chain-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/evidence-chain-contract.js +12 -0
- package/core/dist/runtime-v2/types/evidence-chain-contract.js.map +1 -1
- package/core/package.json +1 -1
- package/dist/installers/codex-host-installer.d.ts +17 -7
- package/dist/installers/codex-host-installer.d.ts.map +1 -1
- package/dist/installers/codex-host-installer.js +78 -194
- package/dist/installers/codex-host-installer.js.map +1 -1
- package/host-runtime/dist/codex-disclosure.d.ts +23 -0
- package/host-runtime/dist/codex-disclosure.js +60 -0
- package/host-runtime/dist/codex-ingestion-consent.d.ts +111 -0
- package/host-runtime/dist/codex-ingestion-consent.js +223 -0
- package/host-runtime/dist/codex-legacy-registration.d.ts +18 -0
- package/host-runtime/dist/codex-legacy-registration.js +34 -0
- package/host-runtime/dist/codex-transcript-locate.d.ts +16 -0
- package/host-runtime/dist/codex-transcript-locate.js +106 -0
- package/host-runtime/dist/codex-worker-status.d.ts +24 -0
- package/host-runtime/dist/codex-worker-status.js +52 -0
- package/host-runtime/dist/governance-observation-store.d.ts +77 -1
- package/host-runtime/dist/governance-observation-store.js +188 -3
- package/host-runtime/dist/governance-signal-admission.d.ts +26 -0
- package/host-runtime/dist/governance-signal-admission.js +47 -0
- package/host-runtime/dist/index.d.ts +5 -0
- package/host-runtime/dist/index.js +14 -0
- package/host-runtime/package.json +1 -1
- package/package.json +1 -1
- package/pd-cli/dist/commands/codex-ingest-quarantine.d.ts +12 -0
- package/pd-cli/dist/commands/codex-ingest-quarantine.d.ts.map +1 -0
- package/pd-cli/dist/commands/codex-ingest-quarantine.js +116 -0
- package/pd-cli/dist/commands/codex-ingest-quarantine.js.map +1 -0
- package/pd-cli/dist/commands/codex-setup.d.ts +46 -0
- package/pd-cli/dist/commands/codex-setup.d.ts.map +1 -0
- package/pd-cli/dist/commands/codex-setup.js +423 -0
- package/pd-cli/dist/commands/codex-setup.js.map +1 -0
- package/pd-cli/dist/commands/health-codex.d.ts.map +1 -1
- package/pd-cli/dist/commands/health-codex.js +350 -67
- package/pd-cli/dist/commands/health-codex.js.map +1 -1
- package/pd-cli/dist/index.js +51 -0
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +331 -331
- package/release-manager/dist/installers/codex-host-installer.d.ts +17 -7
- package/release-manager/dist/installers/codex-host-installer.d.ts.map +1 -1
- package/release-manager/dist/installers/codex-host-installer.js +78 -194
- package/release-manager/dist/installers/codex-host-installer.js.map +1 -1
- package/release-manager/package.json +1 -1
|
@@ -93,7 +93,7 @@ function ingestTranscriptDelta({ fallbackRootSessionId, canonicalPath, identity,
|
|
|
93
93
|
if (offset > window.fileSize) {
|
|
94
94
|
// The file shrank below the committed cursor: replaced or truncated —
|
|
95
95
|
// never guess; hold the checkpoint and degrade explicitly.
|
|
96
|
-
return { status: 'degraded', reason: 'checkpoint_inconsistent', nextAction: 'the transcript is shorter than the committed checkpoint;
|
|
96
|
+
return { status: 'degraded', reason: 'checkpoint_inconsistent', nextAction: 'the transcript is shorter than the committed checkpoint; quarantine the stale rollout records via `pd codex ingest quarantine` or re-ingest from a fresh rollout.', warnings: [] };
|
|
97
97
|
}
|
|
98
98
|
const byteBoundReached = offset + window.bytes.length < window.fileSize;
|
|
99
99
|
const decoded = decodeTranscriptWindow({
|
|
@@ -120,7 +120,7 @@ function ingestTranscriptDelta({ fallbackRootSessionId, canonicalPath, identity,
|
|
|
120
120
|
}
|
|
121
121
|
const degradations = [];
|
|
122
122
|
if (decoded.stop.kind === 'malformed') {
|
|
123
|
-
degradations.push({ reason: 'transcript_record_malformed', ...(decoded.stop.ordinal !== null ? { ordinal: decoded.stop.ordinal } : {}), nextAction: 'the record is stable-invalid;
|
|
123
|
+
degradations.push({ reason: 'transcript_record_malformed', ...(decoded.stop.ordinal !== null ? { ordinal: decoded.stop.ordinal } : {}), nextAction: 'the record is stable-invalid; quarantine this record via `pd codex ingest quarantine` (dry-run default). Later records remain as lag.' });
|
|
124
124
|
}
|
|
125
125
|
else if (decoded.stop.kind === 'oversized_record') {
|
|
126
126
|
degradations.push({ reason: 'transcript_record_too_large', nextAction: 'a single transcript record exceeds the bounded-read window; inspect the rollout file.' });
|
|
@@ -164,7 +164,7 @@ function ingestTranscriptDelta({ fallbackRootSessionId, canonicalPath, identity,
|
|
|
164
164
|
return {
|
|
165
165
|
status: 'degraded',
|
|
166
166
|
reason: 'transcript_record_malformed',
|
|
167
|
-
nextAction: 'the record is stable-invalid;
|
|
167
|
+
nextAction: 'the record is stable-invalid; quarantine this record via `pd codex ingest quarantine` (dry-run default). Later records remain as lag.',
|
|
168
168
|
warnings,
|
|
169
169
|
};
|
|
170
170
|
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
export type CodexTranscriptLookup = {
|
|
2
|
-
ok: true;
|
|
3
|
-
transcriptPath: string;
|
|
4
|
-
} | {
|
|
5
|
-
ok: false;
|
|
6
|
-
reason: 'catch_up_rollout_identity_invalid' | 'catch_up_sessions_root_missing' | 'catch_up_transcript_missing' | 'catch_up_transcript_ambiguous' | 'catch_up_lookup_exhausted';
|
|
7
|
-
nextAction: string;
|
|
8
|
-
};
|
|
9
1
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
2
|
+
* Codex transcript locator — Slice D (PRI-625) moved the implementation to
|
|
3
|
+
* @principles/host-runtime (codex-transcript-locate) so the §15 health
|
|
4
|
+
* service computes per-rollout lag with the SAME locator the catch-up path
|
|
5
|
+
* uses. This module re-exports it for compatibility with existing importers.
|
|
12
6
|
*/
|
|
13
|
-
export
|
|
7
|
+
export { locateCodexTranscriptByRolloutIdentity, parseRolloutFileName } from '@principles/host-runtime';
|
|
8
|
+
export type { CodexTranscriptLookup } from '@principles/host-runtime';
|
|
@@ -1,75 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codex transcript locator
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* rollout back to its transcript file. This is NOT session discovery: the
|
|
7
|
-
* lookup searches for the EXACT rollout uuid of a rollout the authenticated
|
|
8
|
-
* Workspace hook previously delivered (only hooks write checkpoints). It
|
|
9
|
-
* never guesses a "latest session", never returns a partial match, and
|
|
10
|
-
* refuses ambiguities (ADR-0020 §11.2 / SPEC §9).
|
|
2
|
+
* Codex transcript locator — Slice D (PRI-625) moved the implementation to
|
|
3
|
+
* @principles/host-runtime (codex-transcript-locate) so the §15 health
|
|
4
|
+
* service computes per-rollout lag with the SAME locator the catch-up path
|
|
5
|
+
* uses. This module re-exports it for compatibility with existing importers.
|
|
11
6
|
*/
|
|
12
|
-
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import { parseRolloutFileName } from './transcript-path.js';
|
|
15
|
-
/** Bounded walk: hard cap on visited directory entries so a pathological sessions tree cannot stall the worker. */
|
|
16
|
-
const MAX_LOOKUP_ENTRIES = 5000;
|
|
17
|
-
const ROLLOUT_IDENTITY_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
18
|
-
/**
|
|
19
|
-
* Resolve one previously-authenticated rollout identity to its transcript
|
|
20
|
-
* path by exact-uuid filename match under `<codexHome>/sessions`.
|
|
21
|
-
*/
|
|
22
|
-
export function locateCodexTranscriptByRolloutIdentity(codexHome, rolloutIdentity) {
|
|
23
|
-
if (!ROLLOUT_IDENTITY_PATTERN.test(rolloutIdentity)) {
|
|
24
|
-
return { ok: false, reason: 'catch_up_rollout_identity_invalid', nextAction: 'the checkpointed rollout identity is not a rollout uuid; inspect the workspace trajectory database.' };
|
|
25
|
-
}
|
|
26
|
-
const sessionsRoot = path.join(codexHome, 'sessions');
|
|
27
|
-
let rootStats;
|
|
28
|
-
try {
|
|
29
|
-
rootStats = fs.statSync(sessionsRoot);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return { ok: false, reason: 'catch_up_sessions_root_missing', nextAction: 'the configured CODEX_HOME has no sessions root; verify the Codex home used by the hook and by catch-up matches.' };
|
|
33
|
-
}
|
|
34
|
-
if (!rootStats.isDirectory()) {
|
|
35
|
-
return { ok: false, reason: 'catch_up_sessions_root_missing', nextAction: 'the configured CODEX_HOME sessions path is not a directory; verify the Codex home configuration.' };
|
|
36
|
-
}
|
|
37
|
-
const matches = [];
|
|
38
|
-
let visited = 0;
|
|
39
|
-
const stack = [sessionsRoot];
|
|
40
|
-
while (stack.length > 0 && matches.length < 2) {
|
|
41
|
-
const dir = stack.pop();
|
|
42
|
-
if (dir === undefined)
|
|
43
|
-
break;
|
|
44
|
-
let entries;
|
|
45
|
-
try {
|
|
46
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
continue; // unreadable subtree — other subtrees may still hold the rollout
|
|
50
|
-
}
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
visited += 1;
|
|
53
|
-
if (visited > MAX_LOOKUP_ENTRIES) {
|
|
54
|
-
return { ok: false, reason: 'catch_up_lookup_exhausted', nextAction: 'the sessions tree exceeded the bounded catch-up lookup; keep CODEX_HOME/sessions pruned or catch up rollouts manually.' };
|
|
55
|
-
}
|
|
56
|
-
if (entry.isDirectory()) {
|
|
57
|
-
stack.push(path.join(dir, entry.name));
|
|
58
|
-
}
|
|
59
|
-
else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
60
|
-
if (parseRolloutFileName(entry.name) === rolloutIdentity) {
|
|
61
|
-
matches.push(path.join(dir, entry.name));
|
|
62
|
-
if (matches.length >= 2)
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (matches.length === 0) {
|
|
69
|
-
return { ok: false, reason: 'catch_up_transcript_missing', nextAction: 'the checkpointed rollout has no transcript under the Codex sessions root (rotated or cleaned by Codex); its committed observations remain, the pending lag cannot be recovered.' };
|
|
70
|
-
}
|
|
71
|
-
if (matches.length > 1) {
|
|
72
|
-
return { ok: false, reason: 'catch_up_transcript_ambiguous', nextAction: 'multiple transcripts match the rollout identity; refuse to guess — inspect the Codex sessions tree.' };
|
|
73
|
-
}
|
|
74
|
-
return { ok: true, transcriptPath: matches[0] };
|
|
75
|
-
}
|
|
7
|
+
export { locateCodexTranscriptByRolloutIdentity, parseRolloutFileName } from '@principles/host-runtime';
|
|
@@ -20,6 +20,6 @@ export type TranscriptPathValidation = {
|
|
|
20
20
|
reason: 'transcript_path_invalid' | 'transcript_path_outside_codex_home';
|
|
21
21
|
nextAction: string;
|
|
22
22
|
};
|
|
23
|
-
|
|
24
|
-
export
|
|
23
|
+
import { parseRolloutFileName } from '@principles/host-runtime';
|
|
24
|
+
export { parseRolloutFileName };
|
|
25
25
|
export declare function validateCodexTranscriptPath(transcriptPath: string, codexHome: string): TranscriptPathValidation;
|
|
@@ -16,27 +16,10 @@
|
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { canonicalizePath } from './codex-home.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
}
|
|
19
|
+
// PRI-625 Slice D: the rollout filename contract has ONE implementation,
|
|
20
|
+
// moved to @principles/host-runtime with the transcript locator.
|
|
21
|
+
import { parseRolloutFileName } from '@principles/host-runtime';
|
|
22
|
+
export { parseRolloutFileName };
|
|
40
23
|
function isAbsolutePath(value) {
|
|
41
24
|
return path.isAbsolute(value);
|
|
42
25
|
}
|
|
@@ -45,15 +45,4 @@ export interface CodexWorkerStatusEvaluation {
|
|
|
45
45
|
readonly reason?: string;
|
|
46
46
|
readonly nextAction?: string;
|
|
47
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;
|
|
48
|
+
export { computeCodexWorkerStatusMode } from '@principles/host-runtime';
|
|
@@ -234,37 +234,7 @@ export async function runCodexWorkspaceWorkerCycle(options) {
|
|
|
234
234
|
report: { catchUp, reconcile, diagnostician, downstream },
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
}
|
|
237
|
+
// Slice D (PRI-625): the mode authority moved to host-runtime so the CLI and
|
|
238
|
+
// Console §15 health surfaces share ONE semantics with the worker. Re-exported
|
|
239
|
+
// here for compatibility with every existing importer.
|
|
240
|
+
export { computeCodexWorkerStatusMode } from '@principles/host-runtime';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/codex-adapter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexGovernanceHealthModel — Console adapter for the §15 Codex governance
|
|
3
|
+
* health block (Codex Governance Closure Slice D, PRI-625; review round 2
|
|
4
|
+
* authority convergence).
|
|
5
|
+
*
|
|
6
|
+
* The readiness computation has ONE authority: the production CLI
|
|
7
|
+
* `pd health --host codex --json` (packages/pd-cli health-codex.ts). This
|
|
8
|
+
* Console model executes that command in a fresh subprocess against the
|
|
9
|
+
* workspace and returns its report verbatim — the Console MUST NOT copy the
|
|
10
|
+
* readiness calculation, or CLI and Console could disagree about the same
|
|
11
|
+
* governance facts (P4: one source of truth).
|
|
12
|
+
*
|
|
13
|
+
* Failure is explicit (rc-9): if the CLI cannot run, the model returns
|
|
14
|
+
* `status: 'unknown'` with `ready: false` and a `health_collection_failed`
|
|
15
|
+
* blocker — "unable to determine health" is never rendered as healthy.
|
|
16
|
+
*
|
|
17
|
+
* This is a *verbatim* report passthrough, not a projection: no field is
|
|
18
|
+
* recomputed or filtered here.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The report shape mirrors the CLI's CodexHealthReport exactly (the Console
|
|
22
|
+
* renders it read-only). Declared structurally so a CLI contract change
|
|
23
|
+
* surfaces as a Console compile-time review point, not a silent drift.
|
|
24
|
+
*/
|
|
25
|
+
export interface CodexGovernanceHealth {
|
|
26
|
+
/** 'ok' = the CLI authority produced a real report (verbatim passthrough). */
|
|
27
|
+
status: 'ok';
|
|
28
|
+
generatedAt: string;
|
|
29
|
+
host: 'codex';
|
|
30
|
+
workspace: string;
|
|
31
|
+
adapterVersion: string;
|
|
32
|
+
runtimeVersion: string;
|
|
33
|
+
codexIngestionMinVersion: string;
|
|
34
|
+
codexIngestionVerifiedVersion: string;
|
|
35
|
+
ingestionFlag: {
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
source: string;
|
|
38
|
+
};
|
|
39
|
+
consent: {
|
|
40
|
+
state: string;
|
|
41
|
+
disclosureStale?: boolean;
|
|
42
|
+
failureReason?: string;
|
|
43
|
+
nextAction?: string;
|
|
44
|
+
};
|
|
45
|
+
workspaceInit: {
|
|
46
|
+
initialized: boolean;
|
|
47
|
+
reason?: string;
|
|
48
|
+
};
|
|
49
|
+
hooksTrust: {
|
|
50
|
+
detectable: boolean;
|
|
51
|
+
trusted?: boolean;
|
|
52
|
+
reason?: string;
|
|
53
|
+
};
|
|
54
|
+
dualRegistration: {
|
|
55
|
+
detected: boolean;
|
|
56
|
+
legacyAsyncPostToolUse?: boolean;
|
|
57
|
+
nextAction?: string;
|
|
58
|
+
};
|
|
59
|
+
worker: {
|
|
60
|
+
mode: string;
|
|
61
|
+
registeredInInstallManifest: boolean;
|
|
62
|
+
reason?: string;
|
|
63
|
+
nextAction?: string;
|
|
64
|
+
};
|
|
65
|
+
rollouts: {
|
|
66
|
+
checkpoints?: {
|
|
67
|
+
rolloutIdentity: string;
|
|
68
|
+
lagBytes: number | null;
|
|
69
|
+
incompleteTail: boolean;
|
|
70
|
+
updatedAt: string;
|
|
71
|
+
}[];
|
|
72
|
+
reason?: string;
|
|
73
|
+
};
|
|
74
|
+
observations: Record<string, unknown>;
|
|
75
|
+
admissions: Record<string, unknown>;
|
|
76
|
+
diagnosticianTasks: Record<string, unknown>;
|
|
77
|
+
ready: boolean;
|
|
78
|
+
readyBlockers: string[];
|
|
79
|
+
warnings: string[];
|
|
80
|
+
}
|
|
81
|
+
export type CodexGovernanceHealthResult = {
|
|
82
|
+
status: 'ok';
|
|
83
|
+
health: CodexGovernanceHealth;
|
|
84
|
+
} | {
|
|
85
|
+
status: 'unknown';
|
|
86
|
+
ready: false;
|
|
87
|
+
readyBlockers: string[];
|
|
88
|
+
reason: string;
|
|
89
|
+
nextAction: string;
|
|
90
|
+
productClaim: 'degraded';
|
|
91
|
+
};
|
|
92
|
+
export declare class CodexGovernanceHealthModel {
|
|
93
|
+
private readonly workspaceDir;
|
|
94
|
+
private readonly cliEntryOverride;
|
|
95
|
+
constructor(workspaceDir: string, cliEntryOverride?: string);
|
|
96
|
+
collect(): Promise<CodexGovernanceHealthResult>;
|
|
97
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexGovernanceHealthModel — Console adapter for the §15 Codex governance
|
|
3
|
+
* health block (Codex Governance Closure Slice D, PRI-625; review round 2
|
|
4
|
+
* authority convergence).
|
|
5
|
+
*
|
|
6
|
+
* The readiness computation has ONE authority: the production CLI
|
|
7
|
+
* `pd health --host codex --json` (packages/pd-cli health-codex.ts). This
|
|
8
|
+
* Console model executes that command in a fresh subprocess against the
|
|
9
|
+
* workspace and returns its report verbatim — the Console MUST NOT copy the
|
|
10
|
+
* readiness calculation, or CLI and Console could disagree about the same
|
|
11
|
+
* governance facts (P4: one source of truth).
|
|
12
|
+
*
|
|
13
|
+
* Failure is explicit (rc-9): if the CLI cannot run, the model returns
|
|
14
|
+
* `status: 'unknown'` with `ready: false` and a `health_collection_failed`
|
|
15
|
+
* blocker — "unable to determine health" is never rendered as healthy.
|
|
16
|
+
*
|
|
17
|
+
* This is a *verbatim* report passthrough, not a projection: no field is
|
|
18
|
+
* recomputed or filtered here.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { promisify } from 'node:util';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
function resolvePdCliEntry() {
|
|
28
|
+
// pd-cli dist entry, resolvable through the workspace node_modules links.
|
|
29
|
+
try {
|
|
30
|
+
const pkgJsonPath = require.resolve('@principles/pd-cli/package.json');
|
|
31
|
+
const packageRoot = path.dirname(pkgJsonPath);
|
|
32
|
+
const entry = path.join(packageRoot, 'dist', 'index.js');
|
|
33
|
+
const packageRootDir = path.resolve(packageRoot);
|
|
34
|
+
if (!path.resolve(entry).startsWith(packageRootDir + path.sep))
|
|
35
|
+
return null;
|
|
36
|
+
return fs.existsSync(entry) ? entry : null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class CodexGovernanceHealthModel {
|
|
43
|
+
workspaceDir;
|
|
44
|
+
cliEntryOverride;
|
|
45
|
+
constructor(workspaceDir, cliEntryOverride) {
|
|
46
|
+
this.workspaceDir = workspaceDir;
|
|
47
|
+
this.cliEntryOverride = cliEntryOverride ?? null;
|
|
48
|
+
}
|
|
49
|
+
async collect() {
|
|
50
|
+
const entry = this.cliEntryOverride ?? resolvePdCliEntry();
|
|
51
|
+
if (entry === null) {
|
|
52
|
+
return {
|
|
53
|
+
status: 'unknown',
|
|
54
|
+
ready: false,
|
|
55
|
+
readyBlockers: ['health_collection_failed: pd_cli_unavailable'],
|
|
56
|
+
reason: 'pd_cli_unavailable',
|
|
57
|
+
nextAction: 'Install or build @principles/pd-cli (npm i -g @principles/pd-cli) so the Console can run `pd health --host codex`.',
|
|
58
|
+
productClaim: 'degraded',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const { stdout } = await execFileAsync(process.execPath, [entry, 'health', '--host', 'codex', '--workspace', this.workspaceDir, '--json'], { encoding: 'utf8', timeout: 30_000, windowsHide: true });
|
|
63
|
+
// cli-1: the CLI's --json mode prints exactly one (pretty-printed) JSON
|
|
64
|
+
// object on stdout — parse the whole stdout, not just the first `{` line.
|
|
65
|
+
// runtime-contract-exempt: ERR-001 verbatim passthrough of our own CLI's documented JSON contract — single authority, see PRI-625 review round 2.
|
|
66
|
+
const health = JSON.parse(stdout);
|
|
67
|
+
return { status: 'ok', health };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message.slice(0, 200) : String(error);
|
|
71
|
+
return {
|
|
72
|
+
status: 'unknown',
|
|
73
|
+
ready: false,
|
|
74
|
+
readyBlockers: [`health_collection_failed: ${message}`],
|
|
75
|
+
reason: 'health_collection_failed',
|
|
76
|
+
nextAction: 'Run `pd health --host codex --workspace <dir>` manually to see the structured reason.',
|
|
77
|
+
productClaim: 'degraded',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -143,12 +143,26 @@ export class EvidenceChainConsoleModel {
|
|
|
143
143
|
try {
|
|
144
144
|
trajDb = new Database(trajectoryDbPath, { readonly: true });
|
|
145
145
|
try {
|
|
146
|
-
painEvents = trajDb.prepare('SELECT id, session_id, source, score, reason, severity, origin, confidence, text, created_at, canonical_pain_id, runtime_task_id FROM pain_events ORDER BY created_at DESC LIMIT 100').all();
|
|
146
|
+
painEvents = trajDb.prepare('SELECT id, session_id, source, score, reason, severity, origin, confidence, text, created_at, canonical_pain_id, runtime_task_id, host_kind FROM pain_events ORDER BY created_at DESC LIMIT 100').all();
|
|
147
147
|
}
|
|
148
148
|
catch (colErr) {
|
|
149
149
|
const colMessage = colErr instanceof Error ? colErr.message : String(colErr);
|
|
150
150
|
if (colMessage.includes('no such column')) {
|
|
151
|
-
|
|
151
|
+
// Degrade per missing column (review round 3): a missing host_kind
|
|
152
|
+
// must not drop canonical_pain_id / runtime_task_id — the task join
|
|
153
|
+
// runs through those, and losing them orphans evidence records.
|
|
154
|
+
try {
|
|
155
|
+
painEvents = trajDb.prepare('SELECT id, session_id, source, score, reason, severity, origin, confidence, text, created_at, canonical_pain_id, runtime_task_id, NULL AS host_kind FROM pain_events ORDER BY created_at DESC LIMIT 100').all();
|
|
156
|
+
}
|
|
157
|
+
catch (colErr2) {
|
|
158
|
+
const colMessage2 = colErr2 instanceof Error ? colErr2.message : String(colErr2);
|
|
159
|
+
if (colMessage2.includes('no such column')) {
|
|
160
|
+
painEvents = trajDb.prepare('SELECT id, session_id, source, score, reason, severity, origin, confidence, text, created_at FROM pain_events ORDER BY created_at DESC LIMIT 100').all();
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
throw colErr2;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
152
166
|
}
|
|
153
167
|
else {
|
|
154
168
|
throw colErr;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HealthCheckModel } from '../models/HealthCheckModel.js';
|
|
2
|
+
import { CodexGovernanceHealthModel } from '../models/CodexGovernanceHealthModel.js';
|
|
2
3
|
import { sendSuccess, sendError } from '../utils/response.js';
|
|
3
4
|
const MODEL_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
4
5
|
const models = new Map();
|
|
@@ -11,6 +12,23 @@ function getModel(workspaceDir) {
|
|
|
11
12
|
models.set(workspaceDir, { model, cachedAt: Date.now() });
|
|
12
13
|
return model;
|
|
13
14
|
}
|
|
15
|
+
// PRI-625 Slice D (SPEC §15): Codex governance health comes from the ONE
|
|
16
|
+
// authority — `pd health --host codex --json` — executed by the Console
|
|
17
|
+
// model as a subprocess. Cached per workspace with the same TTL discipline
|
|
18
|
+
// as the system health model. Keyed by workspace: the route can serve
|
|
19
|
+
// multiple workspaces.
|
|
20
|
+
const CODEX_WORKSPACE_CACHE_TTL_MS = MODEL_CACHE_TTL_MS;
|
|
21
|
+
const codexModels = new Map();
|
|
22
|
+
const codexCache = new Map();
|
|
23
|
+
function getCodexModel(workspaceDir) {
|
|
24
|
+
const cached = codexModels.get(workspaceDir);
|
|
25
|
+
if (cached && Date.now() - cached.cachedAt < CODEX_WORKSPACE_CACHE_TTL_MS) {
|
|
26
|
+
return cached.model;
|
|
27
|
+
}
|
|
28
|
+
const model = new CodexGovernanceHealthModel(workspaceDir);
|
|
29
|
+
codexModels.set(workspaceDir, { model, cachedAt: Date.now() });
|
|
30
|
+
return model;
|
|
31
|
+
}
|
|
14
32
|
export async function handleHealthRoute(req, res, options) {
|
|
15
33
|
if (req.method !== 'GET') {
|
|
16
34
|
sendError(res, 405, 'method_not_allowed', 'Only GET method is allowed');
|
|
@@ -19,10 +37,46 @@ export async function handleHealthRoute(req, res, options) {
|
|
|
19
37
|
const model = getModel(options.workspaceDir);
|
|
20
38
|
try {
|
|
21
39
|
const health = await model.checkSystemHealth();
|
|
22
|
-
|
|
40
|
+
// Governance consistency (review round 2 + round 3): a codexGovernance
|
|
41
|
+
// collection failure is reported as status:'unknown' + ready:false +
|
|
42
|
+
// blockers — never omitted and never rendered as healthy (rc-9). The
|
|
43
|
+
// collect call has its OWN catch so a throwing model converts to the
|
|
44
|
+
// unknown block here instead of escaping to the system-health 500 path.
|
|
45
|
+
let codexGovernance;
|
|
46
|
+
const cached = codexCache.get(options.workspaceDir);
|
|
47
|
+
if (cached !== undefined && Date.now() - cached.cachedAt < CODEX_WORKSPACE_CACHE_TTL_MS) {
|
|
48
|
+
codexGovernance = cached.result;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
try {
|
|
52
|
+
const collected = await getCodexModel(options.workspaceDir).collect();
|
|
53
|
+
codexGovernance = collected.status === 'ok' ? collected.health : collected;
|
|
54
|
+
codexCache.set(options.workspaceDir, { result: codexGovernance, cachedAt: Date.now() });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message.slice(0, 200) : String(error);
|
|
58
|
+
codexGovernance = {
|
|
59
|
+
status: 'unknown',
|
|
60
|
+
ready: false,
|
|
61
|
+
readyBlockers: [`health_collection_failed: ${message}`],
|
|
62
|
+
reason: 'health_collection_failed',
|
|
63
|
+
nextAction: 'Run `pd health --host codex --workspace <dir>` manually to see the structured reason.',
|
|
64
|
+
productClaim: 'degraded',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
sendSuccess(res, {
|
|
69
|
+
...health,
|
|
70
|
+
authenticationMode: options.authenticationMode,
|
|
71
|
+
codexGovernance,
|
|
72
|
+
});
|
|
23
73
|
}
|
|
24
|
-
catch (
|
|
25
|
-
|
|
74
|
+
catch (error) {
|
|
75
|
+
// A SYSTEM-health failure (checkSystemHealth threw) is a different fact
|
|
76
|
+
// from a codexGovernance-block failure: it must keep the 500
|
|
77
|
+
// health_check_error contract so status-code-based probes and monitors do
|
|
78
|
+
// not read a broken system health as success (review round 3).
|
|
79
|
+
sendError(res, 500, 'health_check_error', error.message);
|
|
26
80
|
}
|
|
27
81
|
}
|
|
28
82
|
export function disposeHealthModels() {
|
|
@@ -30,4 +84,6 @@ export function disposeHealthModels() {
|
|
|
30
84
|
cached.model.dispose();
|
|
31
85
|
}
|
|
32
86
|
models.clear();
|
|
87
|
+
codexModels.clear();
|
|
88
|
+
codexCache.clear();
|
|
33
89
|
}
|
|
@@ -357,6 +357,10 @@
|
|
|
357
357
|
"chainInternalizationTask": "Internalization Task ID",
|
|
358
358
|
"chainDreamerStatus": "Dreamer Status",
|
|
359
359
|
"fieldTrigger": "Trigger",
|
|
360
|
+
"hostKind": {
|
|
361
|
+
"codex": "Codex",
|
|
362
|
+
"openclaw": "OpenClaw"
|
|
363
|
+
},
|
|
360
364
|
"fieldConclusion": "PD's Conclusion",
|
|
361
365
|
"fieldApplicability": "Applicability",
|
|
362
366
|
"fieldConfidence": "Confidence",
|
|
@@ -357,6 +357,10 @@
|
|
|
357
357
|
"chainInternalizationTask": "内化任务 ID",
|
|
358
358
|
"chainDreamerStatus": "Dreamer 状态",
|
|
359
359
|
"fieldTrigger": "触发行为",
|
|
360
|
+
"hostKind": {
|
|
361
|
+
"codex": "Codex",
|
|
362
|
+
"openclaw": "OpenClaw"
|
|
363
|
+
},
|
|
360
364
|
"fieldConclusion": "PD 的结论",
|
|
361
365
|
"fieldApplicability": "适用/说明",
|
|
362
366
|
"fieldConfidence": "置信度",
|
|
@@ -135,7 +135,7 @@ function EvidenceChainCard({ record, expanded, onToggle, t }) {
|
|
|
135
135
|
const confidenceDisplay = layers.layer2.confidence
|
|
136
136
|
? `${enumLabel('confidence', layers.layer2.confidence.label, t)} (${layers.layer2.confidence.raw.toFixed(2)})`
|
|
137
137
|
: null;
|
|
138
|
-
return (_jsxs(Card, { className: "overflow-hidden", children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(Badge, { variant: stateVariant, children: stateLabel }), _jsx(Badge, { variant: "outline", children: sourceLabel }), record.admissionDecision && (_jsx(Badge, { variant: "secondary", children: enumLabel('admission', record.admissionDecision, t) }))] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsx(Button, { variant: "ghost", size: "sm", "data-testid": `pain-feedback-${record.id}`, onClick: () => {
|
|
138
|
+
return (_jsxs(Card, { className: "overflow-hidden", children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(Badge, { variant: stateVariant, children: stateLabel }), _jsx(Badge, { variant: "outline", children: sourceLabel }), record.hostKind && record.hostKind !== 'unknown' && (_jsx(Badge, { variant: "secondary", children: t(`pages.pain.hostKind.${record.hostKind}`) })), record.admissionDecision && (_jsx(Badge, { variant: "secondary", children: enumLabel('admission', record.admissionDecision, t) }))] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsx(Button, { variant: "ghost", size: "sm", "data-testid": `pain-feedback-${record.id}`, onClick: () => {
|
|
139
139
|
const params = new URLSearchParams({ source: "pain_page", painId: record.id });
|
|
140
140
|
navigate(`/report-problem?${params.toString()}`);
|
|
141
141
|
}, children: t('pages.reportProblem.entryFeedback') }), _jsx("span", { className: "font-mono text-[11px] text-ink-4 whitespace-nowrap", children: formatDate(record.observedAt) })] })] }) }), _jsxs(CardContent, { children: [_jsxs("div", { className: "space-y-3 mb-4", children: [_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldTrigger') }), layer2Empty ? (_jsx("p", { className: "text-ink-3 text-sm italic leading-relaxed", children: t('pages.pain.noHumanSummary') })) : (_jsx(CardTitle, { className: "text-[15px] leading-relaxed", children: layers.layer2.triggerSummary }))] }), layers.layer2.conclusion && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldConclusion') }), _jsx("p", { className: "text-ink text-sm leading-relaxed font-medium", children: layers.layer2.conclusion })] })), layers.layer2.applicability && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldApplicability') }), _jsx("p", { className: "text-ink-2 text-sm leading-relaxed", children: layers.layer2.applicability })] })), confidenceDisplay && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldConfidence') }), _jsx("p", { className: "text-ink-2 text-sm", children: confidenceDisplay })] })), layers.layer2.failureReason && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldFailureReason') }), _jsx("p", { className: "text-danger text-sm leading-relaxed", children: layers.layer2.failureReason })] })), layers.layer2.degradedReason && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldDegradedReason') }), _jsx("p", { className: "text-amber text-sm leading-relaxed", children: layers.layer2.degradedReason })] })), layers.layer2.nextAction && (_jsxs("div", { children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-1", children: t('pages.pain.fieldNextAction') }), _jsx("p", { className: "text-ink-2 text-sm leading-relaxed", children: layers.layer2.nextAction })] }))] }), shouldRenderIntentTensionPanel(layers.layer2.intentTension) && (_jsx(IntentTensionPanel, { tension: layers.layer2.intentTension, recordId: record.id, painId: record.id, taskId: record.linkedTaskId ?? undefined, linkedCandidateId: record.linkedCandidateId ?? undefined, t: t })), _jsxs("div", { className: "mt-4 pt-3 border-t border-line", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: async () => {
|
|
@@ -536,6 +536,8 @@ export interface EvidenceChainRecordData {
|
|
|
536
536
|
dreamerTaskStatus?: string;
|
|
537
537
|
/** PRI-469: optional intent tension from diagnostician artifact (SPEC §16). */
|
|
538
538
|
intentTension?: IntentTensionData;
|
|
539
|
+
/** PRI-625 Slice D: evidence host attribution — openclaw | codex | unknown. */
|
|
540
|
+
hostKind?: 'openclaw' | 'codex' | 'unknown';
|
|
539
541
|
}
|
|
540
542
|
export interface IntentTensionData {
|
|
541
543
|
source: string;
|
|
@@ -1791,6 +1791,13 @@ function validateEvidenceChainRecord(v) {
|
|
|
1791
1791
|
summary: v.summary,
|
|
1792
1792
|
};
|
|
1793
1793
|
// Optional fields — fail loud when present but wrong type (ERR-009)
|
|
1794
|
+
if (Object.hasOwn(v, 'hostKind')) {
|
|
1795
|
+
if (!isString(v.hostKind))
|
|
1796
|
+
return null;
|
|
1797
|
+
if (v.hostKind !== 'openclaw' && v.hostKind !== 'codex' && v.hostKind !== 'unknown')
|
|
1798
|
+
return null;
|
|
1799
|
+
result.hostKind = v.hostKind;
|
|
1800
|
+
}
|
|
1794
1801
|
if (Object.hasOwn(v, 'admissionDecision')) {
|
|
1795
1802
|
if (!isString(v.admissionDecision))
|
|
1796
1803
|
return null;
|