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.
Files changed (77) hide show
  1. package/codex-adapter/dist/ingestion/ingestion.js +3 -3
  2. package/codex-adapter/dist/ingestion/transcript-locate.d.ts +6 -11
  3. package/codex-adapter/dist/ingestion/transcript-locate.js +5 -73
  4. package/codex-adapter/dist/ingestion/transcript-path.d.ts +2 -2
  5. package/codex-adapter/dist/ingestion/transcript-path.js +4 -21
  6. package/codex-adapter/dist/worker/workspace-worker.d.ts +1 -12
  7. package/codex-adapter/dist/worker/workspace-worker.js +4 -34
  8. package/codex-adapter/package.json +1 -1
  9. package/console/dist/server/models/CodexGovernanceHealthModel.d.ts +97 -0
  10. package/console/dist/server/models/CodexGovernanceHealthModel.js +81 -0
  11. package/console/dist/server/models/EvidenceChainConsoleModel.js +16 -2
  12. package/console/dist/server/routes/health.js +59 -3
  13. package/console/dist/ui/i18n/en.json +4 -0
  14. package/console/dist/ui/i18n/zh-CN.json +4 -0
  15. package/console/dist/ui/pages/pain/PainPage.js +1 -1
  16. package/console/dist/ui/utils/validators.d.ts +2 -0
  17. package/console/dist/ui/utils/validators.js +7 -0
  18. package/console/dist/web/assets/app.js +15 -1
  19. package/core/dist/host/index.d.ts +2 -0
  20. package/core/dist/host/index.d.ts.map +1 -1
  21. package/core/dist/host/index.js +3 -0
  22. package/core/dist/host/index.js.map +1 -1
  23. package/core/dist/host/legacy-registration.d.ts +28 -0
  24. package/core/dist/host/legacy-registration.d.ts.map +1 -0
  25. package/core/dist/host/legacy-registration.js +49 -0
  26. package/core/dist/host/legacy-registration.js.map +1 -0
  27. package/core/dist/runtime-v2/__tests__/evidence-chain-host-kind.test.d.ts +2 -0
  28. package/core/dist/runtime-v2/__tests__/evidence-chain-host-kind.test.d.ts.map +1 -0
  29. package/core/dist/runtime-v2/__tests__/evidence-chain-host-kind.test.js +49 -0
  30. package/core/dist/runtime-v2/__tests__/evidence-chain-host-kind.test.js.map +1 -0
  31. package/core/dist/runtime-v2/types/evidence-chain-contract.d.ts +6 -0
  32. package/core/dist/runtime-v2/types/evidence-chain-contract.d.ts.map +1 -1
  33. package/core/dist/runtime-v2/types/evidence-chain-contract.js +12 -0
  34. package/core/dist/runtime-v2/types/evidence-chain-contract.js.map +1 -1
  35. package/core/package.json +1 -1
  36. package/dist/installers/codex-host-installer.d.ts +17 -7
  37. package/dist/installers/codex-host-installer.d.ts.map +1 -1
  38. package/dist/installers/codex-host-installer.js +78 -194
  39. package/dist/installers/codex-host-installer.js.map +1 -1
  40. package/host-runtime/dist/codex-disclosure.d.ts +23 -0
  41. package/host-runtime/dist/codex-disclosure.js +60 -0
  42. package/host-runtime/dist/codex-ingestion-consent.d.ts +111 -0
  43. package/host-runtime/dist/codex-ingestion-consent.js +223 -0
  44. package/host-runtime/dist/codex-legacy-registration.d.ts +18 -0
  45. package/host-runtime/dist/codex-legacy-registration.js +34 -0
  46. package/host-runtime/dist/codex-transcript-locate.d.ts +16 -0
  47. package/host-runtime/dist/codex-transcript-locate.js +106 -0
  48. package/host-runtime/dist/codex-worker-status.d.ts +24 -0
  49. package/host-runtime/dist/codex-worker-status.js +52 -0
  50. package/host-runtime/dist/governance-observation-store.d.ts +77 -1
  51. package/host-runtime/dist/governance-observation-store.js +188 -3
  52. package/host-runtime/dist/governance-signal-admission.d.ts +26 -0
  53. package/host-runtime/dist/governance-signal-admission.js +47 -0
  54. package/host-runtime/dist/index.d.ts +5 -0
  55. package/host-runtime/dist/index.js +14 -0
  56. package/host-runtime/package.json +1 -1
  57. package/package.json +1 -1
  58. package/pd-cli/dist/commands/codex-ingest-quarantine.d.ts +12 -0
  59. package/pd-cli/dist/commands/codex-ingest-quarantine.d.ts.map +1 -0
  60. package/pd-cli/dist/commands/codex-ingest-quarantine.js +116 -0
  61. package/pd-cli/dist/commands/codex-ingest-quarantine.js.map +1 -0
  62. package/pd-cli/dist/commands/codex-setup.d.ts +46 -0
  63. package/pd-cli/dist/commands/codex-setup.d.ts.map +1 -0
  64. package/pd-cli/dist/commands/codex-setup.js +423 -0
  65. package/pd-cli/dist/commands/codex-setup.js.map +1 -0
  66. package/pd-cli/dist/commands/health-codex.d.ts.map +1 -1
  67. package/pd-cli/dist/commands/health-codex.js +350 -67
  68. package/pd-cli/dist/commands/health-codex.js.map +1 -1
  69. package/pd-cli/dist/index.js +51 -0
  70. package/pd-cli/dist/index.js.map +1 -1
  71. package/pd-cli/package.json +1 -1
  72. package/plugin/dist/bundle.js +331 -331
  73. package/release-manager/dist/installers/codex-host-installer.d.ts +17 -7
  74. package/release-manager/dist/installers/codex-host-installer.d.ts.map +1 -1
  75. package/release-manager/dist/installers/codex-host-installer.js +78 -194
  76. package/release-manager/dist/installers/codex-host-installer.js.map +1 -1
  77. package/release-manager/package.json +1 -1
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Codex conversation-ingestion consent store — Codex Governance Closure
3
+ * Slice D (SPEC rev 2 §17; G2A frozen disclosure).
4
+ *
5
+ * Persists the Owner's recorded consent decision for enabling
6
+ * `codex_conversation_ingestion` in ONE workspace to
7
+ * `<workspace>/.pd/codex-ingestion-consent.json`.
8
+ *
9
+ * Authority model (P4): the FEATURE FLAG stays the runtime authority for
10
+ * whether ingestion runs (the hook gate reads only the flag — consent never
11
+ * sits on the hot path). This store is the GOVERNANCE RECORD that the flag
12
+ * may only be enabled THROUGH the disclosed consent flow (G2A); health uses
13
+ * it to report consent state and to flag `flag_on_without_grant` as a
14
+ * governance warning. Declining must never flip the flag off by side effect
15
+ * — the setup flow owns that ordering, not this store.
16
+ *
17
+ * Scope: per-workspace (the flag and the transcripts it gates are
18
+ * workspace-scoped). This file is consent control state — it never contains
19
+ * any captured conversation text (SPEC §15 "consent state without displaying
20
+ * captured text").
21
+ *
22
+ * Patterns mirror the product-telemetry consent store: missing file is the
23
+ * normal never-asked case; a malformed file fails loud (rc-3/rc-9) because
24
+ * silently treating it as "never asked" could re-prompt against a recorded
25
+ * Owner decision; writes are atomic (temp + rename); unknown/malformed
26
+ * fields are rejected via guards, never `as` (rc-1/rc-2/rc-5).
27
+ */
28
+ import fs from 'node:fs';
29
+ import path from 'node:path';
30
+ import { CODEX_INGESTION_DISCLOSURE_VERSION } from './codex-disclosure.js';
31
+ export const CODEX_INGESTION_CONSENT_FILENAME = 'codex-ingestion-consent.json';
32
+ export const CODEX_INGESTION_CONSENT_SCHEMA_VERSION = '2';
33
+ export function getCodexIngestionConsentPath(workspaceDir) {
34
+ return path.join(path.resolve(workspaceDir), '.pd', CODEX_INGESTION_CONSENT_FILENAME);
35
+ }
36
+ function isDecision(value) {
37
+ return value === 'granted' || value === 'pending' || value === 'revoked' || value === 'failed';
38
+ }
39
+ function isDecidedVia(value) {
40
+ return value === 'pd_codex_setup' || value === 'codex_plugin_setup';
41
+ }
42
+ function isNonShortString(value, max) {
43
+ return typeof value === 'string' && value.length > 0 && value.length <= max;
44
+ }
45
+ function isIsoTimestamp(value) {
46
+ return isNonShortString(value, 40) && !Number.isNaN(Date.parse(value));
47
+ }
48
+ /**
49
+ * Read the consent record. ENOENT is the normal never-asked case
50
+ * (existed=false, record=null). Anything unreadable/malformed fails loud —
51
+ * degrading to "never asked" could re-prompt or re-enable against the
52
+ * Owner's recorded decision.
53
+ */
54
+ export function readCodexIngestionConsent(workspaceDir) {
55
+ const filePath = getCodexIngestionConsentPath(workspaceDir);
56
+ let raw;
57
+ try {
58
+ raw = fs.readFileSync(filePath, 'utf8');
59
+ }
60
+ catch (error) {
61
+ const codeValue = typeof error === 'object' && error !== null && Object.hasOwn(error, 'code')
62
+ ? error.code
63
+ : undefined;
64
+ if (codeValue === 'ENOENT') {
65
+ return { ok: true, existed: false, record: null };
66
+ }
67
+ const code = typeof codeValue === 'string' ? codeValue : String(error);
68
+ return {
69
+ ok: false,
70
+ reason: `codex_ingestion_consent_unreadable: ${code.slice(0, 120)}`,
71
+ nextAction: `Check permissions on ${filePath}`,
72
+ };
73
+ }
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(raw);
77
+ }
78
+ catch {
79
+ return {
80
+ ok: false,
81
+ reason: 'codex_ingestion_consent_malformed_json',
82
+ nextAction: `Fix or delete ${filePath} (delete = consent returns to not_present; the ingestion flag itself is unchanged)`,
83
+ };
84
+ }
85
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
86
+ return {
87
+ ok: false,
88
+ reason: 'codex_ingestion_consent_malformed_shape',
89
+ nextAction: `Fix or delete ${filePath} (delete = consent returns to not_present; the ingestion flag itself is unchanged)`,
90
+ };
91
+ }
92
+ const obj = parsed;
93
+ const allowedKeys = ['decision', 'disclosureVersion', 'decidedAt', 'decidedVia', 'failureReason', 'schemaVersion'];
94
+ const errors = [];
95
+ for (const key of Object.keys(obj)) {
96
+ if (!allowedKeys.includes(key))
97
+ errors.push(`unknown field '${key}'`);
98
+ }
99
+ if (!isDecision(obj.decision))
100
+ errors.push('decision must be pending|granted|revoked|failed');
101
+ if (!isNonShortString(obj.disclosureVersion, 40))
102
+ errors.push('disclosureVersion must be a short non-empty string');
103
+ if (!isIsoTimestamp(obj.decidedAt))
104
+ errors.push('decidedAt must be a parseable ISO-8601 string');
105
+ if (!isDecidedVia(obj.decidedVia))
106
+ errors.push('decidedVia must be pd_codex_setup|codex_plugin_setup');
107
+ if (obj.failureReason !== undefined && !isNonShortString(obj.failureReason, 200)) {
108
+ errors.push('failureReason must be a non-empty string (≤200 chars) when present');
109
+ }
110
+ if (obj.schemaVersion !== CODEX_INGESTION_CONSENT_SCHEMA_VERSION) {
111
+ errors.push(`schemaVersion must be '${CODEX_INGESTION_CONSENT_SCHEMA_VERSION}'`);
112
+ }
113
+ if (errors.length > 0) {
114
+ return {
115
+ ok: false,
116
+ reason: `codex_ingestion_consent_malformed: ${errors.join('; ')}`,
117
+ nextAction: `Fix or delete ${filePath} (delete = consent returns to not_present; the ingestion flag itself is unchanged)`,
118
+ };
119
+ }
120
+ // Post-validation reconstruction from guard-narrowed fields — no `as` on
121
+ // the untrusted parsed object (rc-2). The errors check above proves every
122
+ // guard passes; the re-invoked guards here satisfy the type system the
123
+ // same way the telemetry consent store does.
124
+ return {
125
+ ok: true,
126
+ existed: true,
127
+ record: {
128
+ decision: isDecision(obj.decision) ? obj.decision : 'failed',
129
+ disclosureVersion: isNonShortString(obj.disclosureVersion, 40) ? obj.disclosureVersion : '',
130
+ decidedAt: isIsoTimestamp(obj.decidedAt) ? obj.decidedAt : '',
131
+ decidedVia: isDecidedVia(obj.decidedVia) ? obj.decidedVia : 'pd_codex_setup',
132
+ ...(isNonShortString(obj.failureReason, 200) && obj.failureReason !== undefined ? { failureReason: obj.failureReason } : {}),
133
+ schemaVersion: CODEX_INGESTION_CONSENT_SCHEMA_VERSION,
134
+ },
135
+ };
136
+ }
137
+ /**
138
+ * Record an explicit consent decision made AFTER the disclosure was
139
+ * presented (the setup flow presents the frozen text before calling this).
140
+ * The flag itself is intentionally untouched — enabling it is the setup
141
+ * flow's explicit, ordered step.
142
+ */
143
+ export function recordCodexIngestionConsent(workspaceDir, input) {
144
+ // Write-side validation mirrors the read-side guards (review round 3): a
145
+ // record this writer produces must never be rejected by its own reader.
146
+ const errors = [];
147
+ if (input.decidedAt !== undefined && !isIsoTimestamp(input.decidedAt))
148
+ errors.push('decidedAt must be a parseable ISO-8601 string when provided');
149
+ if (input.decision === 'failed' && (input.failureReason === undefined || input.failureReason.trim().length === 0)) {
150
+ errors.push('decision=failed requires a non-empty failureReason');
151
+ }
152
+ if (input.failureReason !== undefined && input.decision !== 'failed' && input.failureReason.trim().length === 0) {
153
+ errors.push('failureReason must be non-empty when provided');
154
+ }
155
+ if (errors.length > 0) {
156
+ return {
157
+ ok: false,
158
+ reason: 'codex_ingestion_consent_input_invalid: ' + errors.join('; '),
159
+ nextAction: 'Fix the recordCodexIngestionConsent arguments at the call site.',
160
+ };
161
+ }
162
+ const filePath = getCodexIngestionConsentPath(workspaceDir);
163
+ const record = {
164
+ decision: input.decision,
165
+ disclosureVersion: CODEX_INGESTION_DISCLOSURE_VERSION,
166
+ decidedAt: input.decidedAt ?? new Date().toISOString(),
167
+ decidedVia: input.decidedVia,
168
+ ...(input.failureReason !== undefined ? { failureReason: input.failureReason.slice(0, 200) } : {}),
169
+ schemaVersion: CODEX_INGESTION_CONSENT_SCHEMA_VERSION,
170
+ };
171
+ const dir = path.dirname(filePath);
172
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
173
+ try {
174
+ fs.mkdirSync(dir, { recursive: true });
175
+ fs.writeFileSync(tmpPath, `${JSON.stringify(record, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
176
+ fs.renameSync(tmpPath, filePath);
177
+ return { ok: true, record };
178
+ }
179
+ catch (error) {
180
+ const message = error instanceof Error ? error.message : String(error);
181
+ try {
182
+ fs.rmSync(tmpPath, { force: true });
183
+ }
184
+ catch {
185
+ // best-effort cleanup; the write failure below is the loud signal
186
+ }
187
+ return {
188
+ ok: false,
189
+ reason: `codex_ingestion_consent_write_failed: ${message.slice(0, 200)}`,
190
+ nextAction: `Check permissions on ${dir}`,
191
+ };
192
+ }
193
+ }
194
+ /**
195
+ * Combine the consent record with the ingestion flag into the health-surface
196
+ * state (SPEC §15). Every flag×record combination maps to exactly one
197
+ * interpretable state — none of them reads as silently healthy:
198
+ *
199
+ * record granted + flag any → 'granted' (activation succeeded)
200
+ * record revoked + flag any → 'revoked' (Owner said no; flag-off
201
+ * path also regularizes the flag)
202
+ * record failed + flag any → 'failed' (activation did not land;
203
+ * failureReason explains why)
204
+ * record pending + flag off → 'pending' (decision recorded, activation not yet applied)
205
+ * record pending + flag on → 'pending' (activation applied, terminal write pending)
206
+ * no record + flag on → 'flag_on_without_grant' (governance warning)
207
+ * no record + flag off → 'not_present'
208
+ */
209
+ export function deriveCodexIngestionConsentState(record, ingestionFlagEnabled) {
210
+ if (record === null) {
211
+ return ingestionFlagEnabled ? 'flag_on_without_grant' : 'not_present';
212
+ }
213
+ switch (record.decision) {
214
+ case 'granted':
215
+ return 'granted';
216
+ case 'revoked':
217
+ return 'revoked';
218
+ case 'failed':
219
+ return 'failed';
220
+ case 'pending':
221
+ return 'pending';
222
+ }
223
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Legacy Codex global-hook registration detection — Codex Governance Closure
3
+ * Slice D (PRI-625; SPEC rev 2 §17 legacy-installer retirement).
4
+ *
5
+ * ONE authority for the "is a PD-owned legacy global registration present"
6
+ * fact: the pure parser lives in @principles/core/host
7
+ * (parseLegacyCodexHooksRegistration); this wrapper is the FS edge for
8
+ * host-runtime consumers (health/setup surfaces). The retired installer uses
9
+ * the same core parser at its own edge.
10
+ */
11
+ import { type LegacyCodexRegistration } from '@principles/core/host';
12
+ /**
13
+ * Detect a PD-owned legacy global hook registration in ~/.codex/hooks.json.
14
+ * Unreadable/malformed/absent ⇒ detected:false (nothing provably ours to
15
+ * migrate); the health surface reports the unreadable state separately via
16
+ * its own hooks.json presence check.
17
+ */
18
+ export declare function detectLegacyCodexHookRegistration(hooksJsonPathOverride?: string): LegacyCodexRegistration;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Legacy Codex global-hook registration detection — Codex Governance Closure
3
+ * Slice D (PRI-625; SPEC rev 2 §17 legacy-installer retirement).
4
+ *
5
+ * ONE authority for the "is a PD-owned legacy global registration present"
6
+ * fact: the pure parser lives in @principles/core/host
7
+ * (parseLegacyCodexHooksRegistration); this wrapper is the FS edge for
8
+ * host-runtime consumers (health/setup surfaces). The retired installer uses
9
+ * the same core parser at its own edge.
10
+ */
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import * as path from 'node:path';
14
+ import { parseLegacyCodexHooksRegistration } from '@principles/core/host';
15
+ function getCodexHooksJsonPath() {
16
+ return path.join(os.homedir(), '.codex', 'hooks.json');
17
+ }
18
+ /**
19
+ * Detect a PD-owned legacy global hook registration in ~/.codex/hooks.json.
20
+ * Unreadable/malformed/absent ⇒ detected:false (nothing provably ours to
21
+ * migrate); the health surface reports the unreadable state separately via
22
+ * its own hooks.json presence check.
23
+ */
24
+ export function detectLegacyCodexHookRegistration(hooksJsonPathOverride) {
25
+ const hooksJsonPath = hooksJsonPathOverride ?? getCodexHooksJsonPath();
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
29
+ }
30
+ catch {
31
+ return { detected: false, legacyAsyncPostToolUse: false };
32
+ }
33
+ return parseLegacyCodexHooksRegistration(parsed);
34
+ }
@@ -0,0 +1,16 @@
1
+ export type CodexTranscriptLookup = {
2
+ ok: true;
3
+ transcriptPath: string; /** file size in bytes at lookup time (null when stat fails); lets health compute lag without its own stat call */
4
+ sizeBytes: number | null;
5
+ } | {
6
+ ok: false;
7
+ reason: 'catch_up_rollout_identity_invalid' | 'catch_up_sessions_root_missing' | 'catch_up_transcript_missing' | 'catch_up_transcript_ambiguous' | 'catch_up_lookup_exhausted';
8
+ nextAction: string;
9
+ };
10
+ /** rollout-<timestamp>-<uuid>.jsonl — returns the rollout uuid, or null when the name is off-contract. */
11
+ export declare function parseRolloutFileName(fileName: string): string | null;
12
+ /**
13
+ * Resolve one previously-authenticated rollout identity to its transcript
14
+ * path by exact-uuid filename match under `<codexHome>/sessions`.
15
+ */
16
+ export declare function locateCodexTranscriptByRolloutIdentity(codexHome: string, rolloutIdentity: string): CodexTranscriptLookup;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Codex transcript locator — moved to host-runtime in Slice D (PRI-625):
3
+ * the §15 health surface must compute per-rollout lag with the SAME locator
4
+ * the catch-up path uses (one truth for "where is this rollout's transcript"),
5
+ * and host-runtime cannot import codex-adapter. The adapter re-exports it.
6
+ *
7
+ * The durable checkpoint stores only the rollout uuid (SPEC §18 scenario 9
8
+ * forbids raw paths in the DB), so catch-up must resolve a checkpointed
9
+ * rollout back to its transcript file. This is NOT session discovery: the
10
+ * lookup searches for the EXACT rollout uuid of a rollout the authenticated
11
+ * Workspace hook previously delivered (only hooks write checkpoints). It
12
+ * never guesses a "latest session", never returns a partial match, and
13
+ * refuses ambiguities (ADR-0020 §11.2 / SPEC §9).
14
+ */
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ /** Bounded walk: hard cap on visited directory entries so a pathological sessions tree cannot stall the worker. */
18
+ const MAX_LOOKUP_ENTRIES = 5000;
19
+ const ROLLOUT_IDENTITY_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
20
+ const UUID_HEX = /^[0-9a-fA-F]+$/;
21
+ function isHex(value) {
22
+ return UUID_HEX.test(value);
23
+ }
24
+ /** rollout-<timestamp>-<uuid>.jsonl — returns the rollout uuid, or null when the name is off-contract. */
25
+ export function parseRolloutFileName(fileName) {
26
+ if (!fileName.startsWith('rollout-') || !fileName.endsWith('.jsonl'))
27
+ return null;
28
+ const stem = fileName.slice('rollout-'.length, -'.jsonl'.length);
29
+ const parts = stem.split('-');
30
+ if (parts.length < 6)
31
+ return null; // at least one timestamp segment + the five uuid groups
32
+ const [a, b, c, d, e] = parts.slice(-5);
33
+ if (a === undefined || b === undefined || c === undefined || d === undefined || e === undefined)
34
+ return null;
35
+ if (a.length !== 8 || b.length !== 4 || c.length !== 4 || d.length !== 4 || e.length !== 12)
36
+ return null;
37
+ if (!isHex(a) || !isHex(b) || !isHex(c) || !isHex(d) || !isHex(e))
38
+ return null;
39
+ return parts.slice(-5).join('-').toLowerCase();
40
+ }
41
+ /**
42
+ * Resolve one previously-authenticated rollout identity to its transcript
43
+ * path by exact-uuid filename match under `<codexHome>/sessions`.
44
+ */
45
+ export function locateCodexTranscriptByRolloutIdentity(codexHome, rolloutIdentity) {
46
+ if (!ROLLOUT_IDENTITY_PATTERN.test(rolloutIdentity)) {
47
+ return { ok: false, reason: 'catch_up_rollout_identity_invalid', nextAction: 'the checkpointed rollout identity is not a rollout uuid; inspect the workspace trajectory database.' };
48
+ }
49
+ const sessionsRoot = path.join(codexHome, 'sessions');
50
+ let rootStats;
51
+ try {
52
+ rootStats = fs.statSync(sessionsRoot);
53
+ }
54
+ catch {
55
+ 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.' };
56
+ }
57
+ if (!rootStats.isDirectory()) {
58
+ 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.' };
59
+ }
60
+ const matches = [];
61
+ let visited = 0;
62
+ const stack = [sessionsRoot];
63
+ while (stack.length > 0 && matches.length < 2) {
64
+ const dir = stack.pop();
65
+ if (dir === undefined)
66
+ break;
67
+ let entries;
68
+ try {
69
+ entries = fs.readdirSync(dir, { withFileTypes: true });
70
+ }
71
+ catch {
72
+ continue; // unreadable subtree — other subtrees may still hold the rollout
73
+ }
74
+ for (const entry of entries) {
75
+ visited += 1;
76
+ if (visited > MAX_LOOKUP_ENTRIES) {
77
+ 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.' };
78
+ }
79
+ if (entry.isDirectory()) {
80
+ stack.push(path.join(dir, entry.name));
81
+ }
82
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
83
+ if (parseRolloutFileName(entry.name) === rolloutIdentity) {
84
+ matches.push(path.join(dir, entry.name));
85
+ if (matches.length >= 2)
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ if (matches.length === 0) {
92
+ 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.' };
93
+ }
94
+ if (matches.length > 1) {
95
+ return { ok: false, reason: 'catch_up_transcript_ambiguous', nextAction: 'multiple transcripts match the rollout identity; refuse to guess — inspect the Codex sessions tree.' };
96
+ }
97
+ const transcriptPath = matches[0];
98
+ let sizeBytes;
99
+ try {
100
+ sizeBytes = fs.statSync(transcriptPath).size;
101
+ }
102
+ catch {
103
+ sizeBytes = null;
104
+ }
105
+ return { ok: true, transcriptPath, sizeBytes };
106
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Codex worker status-mode semantics — moved to host-runtime in Slice D
3
+ * (PRI-625): the §15 health surface needs the SAME mode authority in the CLI,
4
+ * the Console, and the worker itself, and this evaluation only depends on
5
+ * host-neutral inputs (workspace directory, .pd/config.yaml flags, install
6
+ * manifest registration). codex-adapter re-exports it for compatibility.
7
+ *
8
+ * SPEC §15 worker mode, evaluated WITHOUT executing anything (no lease, no
9
+ * LLM, no transcript I/O). `manual_action_required` means no
10
+ * Companion-registered worker serves this workspace — the manual CLI path
11
+ * (catch-up / diagnose / run-once) is the recovery route. 'ready' here means
12
+ * "an automatic worker would run and hold the workspace task leases";
13
+ * live-worker liveness surfacing belongs to the Slice D health surface.
14
+ */
15
+ export type CodexWorkerMode = 'ready' | 'manual_action_required' | 'paused' | 'degraded';
16
+ export interface CodexWorkerStatusEvaluation {
17
+ readonly mode: CodexWorkerMode;
18
+ readonly reason?: string;
19
+ readonly nextAction?: string;
20
+ }
21
+ export declare function computeCodexWorkerStatusMode(input: {
22
+ workspaceDir: string;
23
+ registeredInInstallManifest: boolean;
24
+ }): CodexWorkerStatusEvaluation;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Codex worker status-mode semantics — moved to host-runtime in Slice D
3
+ * (PRI-625): the §15 health surface needs the SAME mode authority in the CLI,
4
+ * the Console, and the worker itself, and this evaluation only depends on
5
+ * host-neutral inputs (workspace directory, .pd/config.yaml flags, install
6
+ * manifest registration). codex-adapter re-exports it for compatibility.
7
+ *
8
+ * SPEC §15 worker mode, evaluated WITHOUT executing anything (no lease, no
9
+ * LLM, no transcript I/O). `manual_action_required` means no
10
+ * Companion-registered worker serves this workspace — the manual CLI path
11
+ * (catch-up / diagnose / run-once) is the recovery route. 'ready' here means
12
+ * "an automatic worker would run and hold the workspace task leases";
13
+ * live-worker liveness surfacing belongs to the Slice D health surface.
14
+ */
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import { loadPdConfigForPlugin } from './pd-config.js';
18
+ import { computeFeatureFlagsFromConfig } from '@principles/core/runtime-v2';
19
+ function directoryExists(dir) {
20
+ try {
21
+ return fs.statSync(dir).isDirectory();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export function computeCodexWorkerStatusMode(input) {
28
+ const workspaceDir = path.resolve(input.workspaceDir);
29
+ if (!directoryExists(workspaceDir)) {
30
+ return { mode: 'degraded', reason: 'workspace_missing', nextAction: 'The workspace directory does not exist; restore it or remove it from the install manifest.' };
31
+ }
32
+ const config = loadPdConfigForPlugin(workspaceDir);
33
+ if (!config.ok) {
34
+ const [first] = config.errors;
35
+ return { mode: 'degraded', reason: `pd_config_invalid:${first?.reason ?? 'unknown'}`, nextAction: first?.nextAction ?? 'Repair .pd/config.yaml.' };
36
+ }
37
+ const { flags } = computeFeatureFlagsFromConfig(config.effective);
38
+ if (flags['host.codex']?.enabled !== true) {
39
+ 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.' };
40
+ }
41
+ if (flags.internalization_auto_consumer?.enabled !== true) {
42
+ return { mode: 'paused', reason: 'internalization_auto_consumer_disabled', nextAction: 'Automatic execution is paused; manual commands remain available: pd diagnose, pd runtime internalization run-once.' };
43
+ }
44
+ if (!input.registeredInInstallManifest) {
45
+ return {
46
+ mode: 'manual_action_required',
47
+ reason: 'workspace_not_in_install_manifest',
48
+ 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.`,
49
+ };
50
+ }
51
+ return { mode: 'ready' };
52
+ }
@@ -6,7 +6,8 @@ export declare const GOVERNANCE_PENDING_TAIL_STALE_MS: number;
6
6
  export type GovernanceObservationKind = 'user_turn' | 'assistant_turn' | 'tool_call';
7
7
  export type GovernanceObservationSource = 'live_hook' | 'transcript';
8
8
  export type GovernanceObservationCompleteness = 'complete' | 'partial';
9
- export type GovernanceRetentionClass = 'operational' | 'promoted' | 'expired' | 'rolled_back';
9
+ /** `quarantined` (Slice D §15): audited, permanently-invalid row; bodies dropped, metadata kept. */
10
+ export type GovernanceRetentionClass = 'operational' | 'promoted' | 'expired' | 'rolled_back' | 'quarantined';
10
11
  export interface GovernanceObservationInput {
11
12
  readonly hostKind: 'codex';
12
13
  readonly rolloutIdentity: string;
@@ -193,4 +194,79 @@ export type ListGovernanceObservationsResult = {
193
194
  } | Degradation;
194
195
  export declare function listGovernanceObservations(input: ListGovernanceObservationsInput): ListGovernanceObservationsResult;
195
196
  export declare function promoteGovernanceEvidence(input: PromoteGovernanceEvidenceInput): PromoteGovernanceEvidenceResult;
197
+ export interface QuarantineGovernanceObservationArgs {
198
+ readonly workspaceDir: string;
199
+ readonly hostKind: 'codex';
200
+ readonly rolloutIdentity: string;
201
+ /** governance_observations.id — `pd codex ingest quarantine --record <id>`. */
202
+ readonly recordId: number;
203
+ /** Why this record is permanently invalid (bounded, stored verbatim). */
204
+ readonly reason: string;
205
+ /** Who ran the quarantine (operator identity string, bounded). */
206
+ readonly operator: string;
207
+ /** false = dry run (default contract): report, never mutate. */
208
+ readonly confirm?: boolean;
209
+ readonly databaseFactory?: ObservationDatabaseFactory;
210
+ }
211
+ export interface QuarantinedRecordSummary {
212
+ readonly id: number;
213
+ readonly kind: string;
214
+ readonly logicalKey: string;
215
+ readonly observedAt: string;
216
+ readonly retentionClass: GovernanceRetentionClass;
217
+ /** SHA-256 over the row's stored content (hex) — computed the same way for dry runs. */
218
+ readonly digest: string;
219
+ /** Bounded neighbor description: `prev=<order|none>;next=<order|none>;record=<order|null>`. */
220
+ readonly gap: string;
221
+ }
222
+ export type QuarantineGovernanceObservationResult = {
223
+ ok: true;
224
+ dryRun: boolean;
225
+ alreadyQuarantined: boolean;
226
+ record: QuarantinedRecordSummary;
227
+ } | {
228
+ ok: false;
229
+ reason: string;
230
+ nextAction: string;
231
+ };
232
+ /**
233
+ * Audited quarantine for a permanently invalid governance observation
234
+ * (SPEC §15). Dry run is the contract default: without `confirm` the store
235
+ * reports what WOULD happen and mutates nothing. With `confirm`:
236
+ * - bodies (visible_text / sanitized_tool_facts_json) are dropped;
237
+ * - retention_class becomes `quarantined` (terminal class — never pruned,
238
+ * never promoted);
239
+ * - digest, reason, operator, timestamp, and the neighbor gap are recorded;
240
+ * - the Codex transcript is never read or touched (this function opens only
241
+ * the workspace trajectory.db).
242
+ * Promoted evidence is refused: Owner-decided evidence leaves only through
243
+ * the Owner governance cleanup commands.
244
+ */
245
+ export declare function quarantineGovernanceObservation(args: QuarantineGovernanceObservationArgs): QuarantineGovernanceObservationResult;
246
+ export interface GovernanceObservationStats {
247
+ readonly operational: number;
248
+ readonly promoted: number;
249
+ readonly quarantined: number;
250
+ readonly terminalOther: number;
251
+ /** Oldest operational observed_at + retention window — when the next row ages out. */
252
+ readonly nextExpiryAt: string | null;
253
+ readonly lastObservationAt: string | null;
254
+ }
255
+ export type ReadGovernanceObservationStatsResult = {
256
+ ok: true;
257
+ stats: GovernanceObservationStats;
258
+ } | {
259
+ ok: false;
260
+ reason: string;
261
+ nextAction: string;
262
+ };
263
+ /**
264
+ * Bounded read-only counts for the §15 health surface. Unknown is reported
265
+ * as a structured degradation — never silently as zero (§15: unknown is not
266
+ * reported as healthy).
267
+ */
268
+ export declare function readGovernanceObservationStats(args: {
269
+ workspaceDir: string;
270
+ databaseFactory?: ObservationDatabaseFactory;
271
+ }): ReadGovernanceObservationStatsResult;
196
272
  export {};