create-principles-disciple 1.133.0 → 1.133.2

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
@@ -26,6 +26,7 @@
26
26
  */
27
27
  import fs from 'node:fs';
28
28
  import path from 'node:path';
29
+ import { createHash } from 'node:crypto';
29
30
  import Database from 'better-sqlite3';
30
31
  import { sanitizeString, sanitizeValue } from '@principles/core/runtime-v2';
31
32
  // ─── Retention policy constants (Owner-approved, SPEC rev 2 §11) ────────────
@@ -33,7 +34,7 @@ export const GOVERNANCE_RETENTION_MAX_TURNS = 32;
33
34
  export const GOVERNANCE_RETENTION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
34
35
  export const GOVERNANCE_PROMOTION_PRECEDING_TURNS = 12;
35
36
  export const GOVERNANCE_PENDING_TAIL_STALE_MS = 7 * 24 * 60 * 60 * 1000;
36
- const GOVERNANCE_OBSERVATION_SCHEMA_VERSION = 2;
37
+ const GOVERNANCE_OBSERVATION_SCHEMA_VERSION = 3;
37
38
  const MAX_TEXT_BOUND = 200; // matches MAX_EVIDENCE_VALUE_CHARS; guards stored identity fields
38
39
  const MAX_JSON_COLUMN = 8_000;
39
40
  function isRecord(value) {
@@ -154,11 +155,19 @@ const V2_ADDED_COLUMNS = [
154
155
  { table: 'governance_observations', name: 'source_order', type: 'INTEGER' },
155
156
  { table: 'governance_observations', name: 'source_ordinal', type: 'INTEGER' },
156
157
  ];
158
+ /** Column additions in v3 (Slice D §15 quarantine: audited recovery metadata). */
159
+ const V3_ADDED_COLUMNS = [
160
+ { table: 'governance_observations', name: 'quarantined_at', type: 'TEXT' },
161
+ { table: 'governance_observations', name: 'quarantine_reason', type: 'TEXT' },
162
+ { table: 'governance_observations', name: 'quarantine_digest', type: 'TEXT' },
163
+ { table: 'governance_observations', name: 'quarantine_operator', type: 'TEXT' },
164
+ { table: 'governance_observations', name: 'quarantine_gap', type: 'TEXT' },
165
+ ];
157
166
  function ensureGovernanceObservationSchema(db) {
158
167
  db.transaction(() => {
159
168
  for (const statement of CREATE_STATEMENTS)
160
169
  db.exec(statement);
161
- for (const column of V2_ADDED_COLUMNS) {
170
+ for (const column of [...V2_ADDED_COLUMNS, ...V3_ADDED_COLUMNS]) {
162
171
  try {
163
172
  db.exec(`ALTER TABLE ${column.table} ADD COLUMN ${column.name} ${column.type}`);
164
173
  }
@@ -460,7 +469,7 @@ export function ingestGovernanceObservations(input) {
460
469
  })();
461
470
  if (result === 'conflict') {
462
471
  return {
463
- ok: false, reason: 'logical_key_content_conflict', nextAction: 'the first committed content was preserved and the observation is marked partial; run the audited quarantine/recovery command once available (Slice D) or re-ingest from a fresh rollout',
472
+ ok: false, reason: 'logical_key_content_conflict', nextAction: 'the first committed content was preserved and the observation is marked partial; run `pd codex ingest quarantine --workspace <path> --rollout <id> --record <id>` (audited, dry-run default) or re-ingest from a fresh rollout',
464
473
  inserted, enriched, duplicates, checkpointCommitted: false, warnings: [`conflict:${conflictKey}`],
465
474
  };
466
475
  }
@@ -694,3 +703,179 @@ export function promoteGovernanceEvidence(input) {
694
703
  close();
695
704
  }
696
705
  }
706
+ const QUARANTINE_REASON_MAX = 200;
707
+ const QUARANTINE_OPERATOR_MAX = 80;
708
+ function observationDigest(row) {
709
+ // Digest the row's stored content exactly as persisted (rc-8: hashing
710
+ // string columns only — never JSON.stringify of untrusted unknowns). The
711
+ // digest lets a future audit recognize the same record if it reappears.
712
+ const hash = createHash('sha256');
713
+ for (const column of ['rollout_identity', 'logical_key', 'transcript_record_key', 'kind', 'source', 'completeness', 'visible_text', 'sanitized_tool_facts_json', 'observed_at']) {
714
+ const value = row[column];
715
+ hash.update(`${column}=`);
716
+ hash.update(value === null || value === undefined ? '<null>' : String(value));
717
+ hash.update('\n');
718
+ }
719
+ return hash.digest('hex');
720
+ }
721
+ /**
722
+ * Audited quarantine for a permanently invalid governance observation
723
+ * (SPEC §15). Dry run is the contract default: without `confirm` the store
724
+ * reports what WOULD happen and mutates nothing. With `confirm`:
725
+ * - bodies (visible_text / sanitized_tool_facts_json) are dropped;
726
+ * - retention_class becomes `quarantined` (terminal class — never pruned,
727
+ * never promoted);
728
+ * - digest, reason, operator, timestamp, and the neighbor gap are recorded;
729
+ * - the Codex transcript is never read or touched (this function opens only
730
+ * the workspace trajectory.db).
731
+ * Promoted evidence is refused: Owner-decided evidence leaves only through
732
+ * the Owner governance cleanup commands.
733
+ */
734
+ export function quarantineGovernanceObservation(args) {
735
+ const { workspaceDir, hostKind, rolloutIdentity, recordId, reason, operator, confirm = false } = args;
736
+ if (!Number.isInteger(recordId) || recordId <= 0) {
737
+ return { ok: false, reason: 'record_id_invalid', nextAction: 'Pass the numeric governance_observations.id (--record <id>); run `pd codex ingest catch-up --json` output or inspect trajectory.db to find it.' };
738
+ }
739
+ if (typeof reason !== 'string' || reason.trim().length === 0 || reason.length > QUARANTINE_REASON_MAX) {
740
+ return { ok: false, reason: 'reason_required', nextAction: `Provide a non-empty reason (≤ ${QUARANTINE_REASON_MAX} chars) describing why the record is permanently invalid.` };
741
+ }
742
+ if (typeof operator !== 'string' || operator.trim().length === 0 || operator.length > QUARANTINE_OPERATOR_MAX) {
743
+ return { ok: false, reason: 'operator_required', nextAction: `Provide the operator identity (≤ ${QUARANTINE_OPERATOR_MAX} chars).` };
744
+ }
745
+ const opened = openStore(workspaceDir, args.databaseFactory);
746
+ if (!('db' in opened))
747
+ return opened;
748
+ const { db, close } = opened;
749
+ try {
750
+ const outcome = db.transaction(() => {
751
+ const rolloutRow = db.prepare('SELECT id FROM governance_rollouts WHERE host_kind = ? AND rollout_identity = ?').get(hostKind, rolloutIdentity);
752
+ if (!isRecord(rolloutRow)) {
753
+ return { ok: false, reason: 'rollout_not_found', nextAction: `No governed rollout '${rolloutIdentity}' for host '${hostKind}' in this workspace; check the identity (see \`pd codex ingest catch-up\` output).` };
754
+ }
755
+ const rolloutRowId = rowField(rolloutRow, 'id');
756
+ const row = db.prepare('SELECT * FROM governance_observations WHERE id = ? AND rollout_row_id = ?').get(recordId, rolloutRowId);
757
+ if (!isRecord(row)) {
758
+ return { ok: false, reason: 'record_not_found', nextAction: `Record ${recordId} does not exist in rollout '${rolloutIdentity}' of this workspace; verify the id.` };
759
+ }
760
+ const retentionClassRaw = String(rowField(row, 'retention_class'));
761
+ // rc-2: the DB column is untrusted — only known classes enter the typed
762
+ // summary; anything else reads as 'expired' semantics for reporting but
763
+ // still fails the quarantinable check below.
764
+ const retentionClass = retentionClassRaw === 'operational' || retentionClassRaw === 'promoted'
765
+ || retentionClassRaw === 'quarantined' || retentionClassRaw === 'rolled_back'
766
+ ? retentionClassRaw
767
+ : 'expired';
768
+ const quarantinedAt = rowField(row, 'quarantined_at');
769
+ const sourceOrder = typeof rowField(row, 'source_order') === 'number' ? rowField(row, 'source_order') : null;
770
+ const neighborQuery = (direction) => {
771
+ if (sourceOrder === null)
772
+ return 'none';
773
+ const comparison = direction === 'prev' ? '<' : '>';
774
+ const ordering = direction === 'prev' ? 'DESC' : 'ASC';
775
+ const neighbor = db.prepare(`SELECT source_order FROM governance_observations
776
+ WHERE rollout_row_id = ? AND source_order IS NOT NULL AND source_order ${comparison} ?
777
+ ORDER BY source_order ${ordering} LIMIT 1`).get(rolloutRowId, sourceOrder);
778
+ const value = isRecord(neighbor) ? rowField(neighbor, 'source_order') : undefined;
779
+ return typeof value === 'number' ? value : 'none';
780
+ };
781
+ const summary = {
782
+ id: recordId,
783
+ kind: String(rowField(row, 'kind')),
784
+ logicalKey: String(rowField(row, 'logical_key')),
785
+ observedAt: String(rowField(row, 'observed_at')),
786
+ retentionClass,
787
+ digest: observationDigest(row),
788
+ gap: `prev=${neighborQuery('prev')};next=${neighborQuery('next')};record=${sourceOrder === null ? 'null' : sourceOrder}`,
789
+ };
790
+ if (typeof quarantinedAt === 'string' && quarantinedAt.length > 0) {
791
+ // Already terminal: report the RECORDED audit digest/gap, not a
792
+ // re-hash of the row (bodies were dropped at quarantine time, so a
793
+ // fresh hash would silently differ from the audited one). dryRun
794
+ // reflects THIS invocation's intent — a dry-run call reports
795
+ // dryRun:true even on an already-quarantined row (review round 3).
796
+ const recordedDigest = rowField(row, 'quarantine_digest');
797
+ const recordedGap = rowField(row, 'quarantine_gap');
798
+ return {
799
+ ok: true, dryRun: !confirm, alreadyQuarantined: true,
800
+ record: {
801
+ ...summary,
802
+ digest: typeof recordedDigest === 'string' && recordedDigest.length > 0 ? recordedDigest : summary.digest,
803
+ gap: typeof recordedGap === 'string' && recordedGap.length > 0 ? recordedGap : summary.gap,
804
+ },
805
+ };
806
+ }
807
+ if (retentionClass === 'promoted') {
808
+ return { ok: false, reason: 'record_is_promoted_evidence', nextAction: 'Promoted evidence is Owner-decided governance evidence; quarantine never touches it. Use the existing Owner governance cleanup commands if the evidence must be removed.' };
809
+ }
810
+ if (retentionClass !== 'operational') {
811
+ return { ok: false, reason: `record_not_quarantinable:${retentionClass}`, nextAction: 'Only operational records can be quarantined; expired and rolled-back rows are already terminal.' };
812
+ }
813
+ if (!confirm) {
814
+ return { ok: true, dryRun: true, alreadyQuarantined: false, record: summary };
815
+ }
816
+ const now = new Date().toISOString();
817
+ db.prepare(`UPDATE governance_observations
818
+ SET visible_text = NULL, sanitized_tool_facts_json = NULL, retention_class = 'quarantined',
819
+ quarantined_at = ?, quarantine_reason = ?, quarantine_digest = ?, quarantine_operator = ?, quarantine_gap = ?
820
+ WHERE id = ? AND retention_class = 'operational'`)
821
+ .run(now, reason, summary.digest, operator, summary.gap, recordId);
822
+ return { ok: true, dryRun: false, alreadyQuarantined: false, record: summary };
823
+ })();
824
+ return outcome;
825
+ }
826
+ catch (error) {
827
+ const detail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
828
+ return { ok: false, reason: `governance_quarantine_failed:${detail}`, nextAction: 'inspect the workspace trajectory database and retry; the transaction rolled back, nothing was partially mutated' };
829
+ }
830
+ finally {
831
+ close();
832
+ }
833
+ }
834
+ /**
835
+ * Bounded read-only counts for the §15 health surface. Unknown is reported
836
+ * as a structured degradation — never silently as zero (§15: unknown is not
837
+ * reported as healthy).
838
+ */
839
+ export function readGovernanceObservationStats(args) {
840
+ const opened = openStore(args.workspaceDir, args.databaseFactory);
841
+ if (!('db' in opened))
842
+ return opened;
843
+ const { db, close } = opened;
844
+ try {
845
+ const counts = db.prepare(`SELECT
846
+ SUM(CASE WHEN retention_class = 'operational' THEN 1 ELSE 0 END) AS operational,
847
+ SUM(CASE WHEN retention_class = 'promoted' THEN 1 ELSE 0 END) AS promoted,
848
+ SUM(CASE WHEN retention_class = 'quarantined' THEN 1 ELSE 0 END) AS quarantined,
849
+ SUM(CASE WHEN retention_class IN ('expired', 'rolled_back') THEN 1 ELSE 0 END) AS terminal_other,
850
+ MIN(CASE WHEN retention_class = 'operational' THEN observed_at END) AS oldest_operational,
851
+ MAX(observed_at) AS last_observation
852
+ FROM governance_observations`).get();
853
+ if (!isRecord(counts)) {
854
+ return { ok: false, reason: 'governance_stats_unavailable', nextAction: 'Inspect the workspace trajectory.db governance_observations table.' };
855
+ }
856
+ const numberOr = (value) => (typeof value === 'number' ? value : 0);
857
+ const stringOr = (value) => (typeof value === 'string' && value.length > 0 ? value : null);
858
+ const oldest = stringOr(own(counts, 'oldest_operational'));
859
+ const nextExpiry = oldest !== null && !Number.isNaN(Date.parse(oldest))
860
+ ? new Date(Date.parse(oldest) + GOVERNANCE_RETENTION_MAX_AGE_MS).toISOString()
861
+ : null;
862
+ return {
863
+ ok: true,
864
+ stats: {
865
+ operational: numberOr(own(counts, 'operational')),
866
+ promoted: numberOr(own(counts, 'promoted')),
867
+ quarantined: numberOr(own(counts, 'quarantined')),
868
+ terminalOther: numberOr(own(counts, 'terminal_other')),
869
+ nextExpiryAt: nextExpiry,
870
+ lastObservationAt: stringOr(own(counts, 'last_observation')),
871
+ },
872
+ };
873
+ }
874
+ catch (error) {
875
+ const detail = error instanceof Error ? error.message.slice(0, 160) : String(error);
876
+ return { ok: false, reason: `governance_stats_failed:${detail}`, nextAction: 'Inspect the workspace trajectory.db; the health surface degraded without mutating anything.' };
877
+ }
878
+ finally {
879
+ close();
880
+ }
881
+ }
@@ -225,4 +225,30 @@ export interface ReconcileGovernanceContinuationResult {
225
225
  * Not a background worker — Slice C's Companion worker and the CLI call this.
226
226
  */
227
227
  export declare function reconcileGovernanceContinuation(input: ReconcileGovernanceContinuationInput): Promise<ReconcileGovernanceContinuationResult>;
228
+ export interface GovernanceAdmissionCounts {
229
+ readonly admitted: number;
230
+ /** Admitted pains with no Diagnostician task link — the §15 orphan count. */
231
+ readonly admittedWithoutTask: number;
232
+ readonly pendingTails: number;
233
+ readonly staleTails: number;
234
+ readonly completedTails: number;
235
+ readonly lastAdmissionAt: string | null;
236
+ }
237
+ export type ReadGovernanceAdmissionCountsResult = {
238
+ ok: true;
239
+ counts: GovernanceAdmissionCounts;
240
+ } | {
241
+ ok: false;
242
+ reason: string;
243
+ nextAction: string;
244
+ };
245
+ /**
246
+ * Bounded read-only counts for the §15 health surface: admitted-pain-without-
247
+ * task count and promotion-tail states. Read-only — reconciliation itself
248
+ * stays in `reconcileGovernanceContinuation`.
249
+ */
250
+ export declare function readGovernanceAdmissionCounts(args: {
251
+ workspaceDir: string;
252
+ databaseFactory?: ObservationDatabaseFactory;
253
+ }): ReadGovernanceAdmissionCountsResult;
228
254
  export {};
@@ -857,3 +857,50 @@ export async function reconcileGovernanceContinuation(input) {
857
857
  close();
858
858
  }
859
859
  }
860
+ /**
861
+ * Bounded read-only counts for the §15 health surface: admitted-pain-without-
862
+ * task count and promotion-tail states. Read-only — reconciliation itself
863
+ * stays in `reconcileGovernanceContinuation`.
864
+ */
865
+ export function readGovernanceAdmissionCounts(args) {
866
+ const opened = openAdmissionStore(args.workspaceDir, args.databaseFactory);
867
+ if (!('db' in opened))
868
+ return opened;
869
+ const { db, close } = opened;
870
+ try {
871
+ const admissions = db.prepare(`SELECT
872
+ SUM(CASE WHEN decision = 'admitted' THEN 1 ELSE 0 END) AS admitted,
873
+ SUM(CASE WHEN decision = 'admitted' AND diagnostician_task_id IS NULL THEN 1 ELSE 0 END) AS admitted_without_task,
874
+ MAX(created_at) AS last_admission
875
+ FROM governance_signal_admissions`).get();
876
+ if (!isRecord(admissions)) {
877
+ return { ok: false, reason: 'governance_admission_counts_unavailable', nextAction: 'Inspect the governance_signal_admissions table in the workspace trajectory.db.' };
878
+ }
879
+ const numberOr = (value) => (typeof value === 'number' ? value : 0);
880
+ const stringOr = (value) => (typeof value === 'string' && value.length > 0 ? value : null);
881
+ const tails = db.prepare(`SELECT
882
+ SUM(CASE WHEN state = 'pending' THEN 1 ELSE 0 END) AS pending,
883
+ SUM(CASE WHEN state = 'stale' THEN 1 ELSE 0 END) AS stale,
884
+ SUM(CASE WHEN state = 'completed' THEN 1 ELSE 0 END) AS completed
885
+ FROM governance_pending_promotion_tails`).get();
886
+ const tailRow = isRecord(tails) ? tails : {};
887
+ return {
888
+ ok: true,
889
+ counts: {
890
+ admitted: numberOr(own(admissions, 'admitted')),
891
+ admittedWithoutTask: numberOr(own(admissions, 'admitted_without_task')),
892
+ pendingTails: numberOr(own(tailRow, 'pending')),
893
+ staleTails: numberOr(own(tailRow, 'stale')),
894
+ completedTails: numberOr(own(tailRow, 'completed')),
895
+ lastAdmissionAt: stringOr(own(admissions, 'last_admission')),
896
+ },
897
+ };
898
+ }
899
+ catch (error) {
900
+ const detail = error instanceof Error ? error.message.slice(0, 160) : String(error);
901
+ return { ok: false, reason: `governance_admission_counts_failed:${detail}`, nextAction: 'Inspect the workspace trajectory.db; the health surface degraded without mutating anything.' };
902
+ }
903
+ finally {
904
+ close();
905
+ }
906
+ }
@@ -24,6 +24,11 @@ export * from './product-telemetry/eligibility.js';
24
24
  export * from './product-telemetry/exporter.js';
25
25
  export * from './product-telemetry/milestone-readers.js';
26
26
  export * from './product-telemetry/service.js';
27
+ export * from './codex-disclosure.js';
28
+ export * from './codex-ingestion-consent.js';
29
+ export * from './codex-worker-status.js';
30
+ export * from './codex-legacy-registration.js';
31
+ export * from './codex-transcript-locate.js';
27
32
  export declare const HOST_RUNTIME_ROUTES: readonly ['before_prompt_build', 'before_tool_call', 'after_tool_call'];
28
33
  export type HostRuntimeRoute = (typeof HOST_RUNTIME_ROUTES)[number];
29
34
  export type HostRuntimePort = (event: HostEvent) => HostEventResult | Promise<HostEventResult>;
@@ -33,6 +33,20 @@ export * from './product-telemetry/eligibility.js';
33
33
  export * from './product-telemetry/exporter.js';
34
34
  export * from './product-telemetry/milestone-readers.js';
35
35
  export * from './product-telemetry/service.js';
36
+ // PRI-625 Slice D: Codex conversation-ingestion consent — G2A frozen
37
+ // disclosure SSoT constant + workspace consent record (governance layer; the
38
+ // runtime gate remains the feature flag alone).
39
+ export * from './codex-disclosure.js';
40
+ export * from './codex-ingestion-consent.js';
41
+ // PRI-625 Slice D: ONE §15 worker-mode authority, moved from codex-adapter so
42
+ // the CLI, the Console, and the worker share the same semantics.
43
+ export * from './codex-worker-status.js';
44
+ // PRI-625 Slice D: ONE legacy-registration predicate (installer refusal,
45
+ // health dualRegistration, and future setup flows all read the same fact).
46
+ export * from './codex-legacy-registration.js';
47
+ // PRI-625 Slice D: transcript locator moved here so the §15 health service
48
+ // computes per-rollout lag with the SAME locator the catch-up path uses.
49
+ export * from './codex-transcript-locate.js';
36
50
  export const HOST_RUNTIME_ROUTES = [
37
51
  'before_prompt_build',
38
52
  'before_tool_call',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-principles-disciple",
3
- "version": "1.133.0",
3
+ "version": "1.133.2",
4
4
  "description": "Interactive CLI installer for Principles Disciple OpenClaw plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,12 @@
1
+ interface CodexIngestQuarantineOptions {
2
+ workspace?: string;
3
+ rollout?: string;
4
+ record?: string;
5
+ reason?: string;
6
+ operator?: string;
7
+ confirm?: boolean;
8
+ json?: boolean;
9
+ }
10
+ export declare function handleCodexIngestQuarantine(options: CodexIngestQuarantineOptions): Promise<void>;
11
+ export {};
12
+ //# sourceMappingURL=codex-ingest-quarantine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-ingest-quarantine.d.ts","sourceRoot":"","sources":["../../src/commands/codex-ingest-quarantine.ts"],"names":[],"mappings":"AAuBA,UAAU,4BAA4B;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAUD,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFtG"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * pd codex ingest quarantine — audited recovery for permanently invalid
3
+ * governance observations (Codex Governance Closure Slice D, PRI-625;
4
+ * SPEC rev 2 §15).
5
+ *
6
+ * Contract (§15 verbatim requirements):
7
+ * - dry run by DEFAULT; `--confirm` is required to actually quarantine;
8
+ * - the quarantine record captures digest, reason, operator, timestamp, and
9
+ * the neighbor gap;
10
+ * - the Codex transcript is NEVER edited or read (only the workspace
11
+ * trajectory.db opens — asserted by the store tests via a port spy);
12
+ * - promoted (Owner-decided) evidence is refused;
13
+ * - --json emits exactly one documented object; failed validation mutates
14
+ * nothing.
15
+ *
16
+ * CLI gate compliance: cli-1 (single JSON object), cli-2 (exit paths stop),
17
+ * cli-5 (failed validation performs no mutation), cli-6 (reason + nextAction
18
+ * on every refusal).
19
+ */
20
+ import * as os from 'os';
21
+ import { quarantineGovernanceObservation } from '@principles/host-runtime';
22
+ import { resolveWorkspaceDir } from '../resolve-workspace.js';
23
+ function defaultOperator() {
24
+ try {
25
+ return os.userInfo().username.slice(0, 40) || 'unknown';
26
+ }
27
+ catch {
28
+ return 'unknown';
29
+ }
30
+ }
31
+ export async function handleCodexIngestQuarantine(options) {
32
+ const generatedAt = new Date().toISOString();
33
+ const refuse = (reason, nextAction) => {
34
+ const report = { generatedAt, host: 'codex', op: 'quarantine', status: 'refused', confirmed: options.confirm === true, reason, nextAction };
35
+ if (options.json) {
36
+ console.log(JSON.stringify(report));
37
+ }
38
+ else {
39
+ console.log('Codex observation quarantine');
40
+ console.log(` status: ${report.status}`);
41
+ console.log(` reason: ${reason}`);
42
+ console.log(` next action: ${nextAction}`);
43
+ }
44
+ process.exitCode = 1;
45
+ };
46
+ const rollout = typeof options.rollout === 'string' ? options.rollout.trim() : '';
47
+ if (rollout.length === 0) {
48
+ refuse('rollout_required', 'Pass --rollout <id> (the rollout identity whose record is invalid).');
49
+ return;
50
+ }
51
+ const recordRaw = typeof options.record === 'string' ? options.record.trim() : '';
52
+ const recordId = /^\d+$/.test(recordRaw) ? Number.parseInt(recordRaw, 10) : Number.NaN;
53
+ if (!Number.isInteger(recordId) || recordId <= 0) {
54
+ refuse('record_required', 'Pass --record <id> with the numeric governance_observations.id to quarantine.');
55
+ return;
56
+ }
57
+ const reason = typeof options.reason === 'string' ? options.reason.trim() : '';
58
+ if (reason.length === 0 || reason.length > 200) {
59
+ refuse('reason_required', 'Pass --reason "<why>" (1-200 chars) describing why the record is permanently invalid.');
60
+ return;
61
+ }
62
+ const operator = (typeof options.operator === 'string' && options.operator.trim().length > 0 ? options.operator.trim() : defaultOperator()).slice(0, 80);
63
+ // resolveWorkspaceDir throws when no workspace can be determined — wrap so
64
+ // --json still emits exactly one structured refusal (cli-1/cli-6).
65
+ let workspaceDir;
66
+ try {
67
+ workspaceDir = resolveWorkspaceDir(options.workspace);
68
+ }
69
+ catch (error) {
70
+ const message = error instanceof Error ? error.message.slice(0, 160) : String(error);
71
+ refuse('workspace_unresolved: ' + message, 'Run from inside a PD workspace or pass -w/--workspace <path> with an initialized .pd directory.');
72
+ return;
73
+ }
74
+ const result = quarantineGovernanceObservation({
75
+ workspaceDir,
76
+ hostKind: 'codex',
77
+ rolloutIdentity: rollout,
78
+ recordId,
79
+ reason,
80
+ operator,
81
+ confirm: options.confirm === true,
82
+ });
83
+ if (!result.ok) {
84
+ refuse(result.reason, result.nextAction);
85
+ return;
86
+ }
87
+ const report = {
88
+ generatedAt,
89
+ host: 'codex',
90
+ op: 'quarantine',
91
+ status: 'ok',
92
+ dryRun: result.dryRun,
93
+ alreadyQuarantined: result.alreadyQuarantined,
94
+ confirmed: options.confirm === true,
95
+ record: result.record,
96
+ ...(result.dryRun ? { nextAction: 'Dry run only — nothing was mutated. Re-run with --confirm to quarantine this record.' } : {}),
97
+ transcriptTouched: false,
98
+ };
99
+ if (options.json) {
100
+ console.log(JSON.stringify(report));
101
+ }
102
+ else {
103
+ console.log('Codex observation quarantine');
104
+ console.log(` status: ${result.dryRun ? 'dry-run' : 'quarantined'}`);
105
+ if (result.alreadyQuarantined)
106
+ console.log(' note: record was already quarantined (idempotent)');
107
+ console.log(` record id: ${result.record.id} (${result.record.kind}, ${result.record.retentionClass})`);
108
+ console.log(` logical key: ${result.record.logicalKey}`);
109
+ console.log(` observed at: ${result.record.observedAt}`);
110
+ console.log(` digest: ${result.record.digest}`);
111
+ console.log(` gap: ${result.record.gap}`);
112
+ if (result.dryRun)
113
+ console.log(' next action: Dry run only — nothing was mutated. Re-run with --confirm to quarantine this record.');
114
+ }
115
+ }
116
+ //# sourceMappingURL=codex-ingest-quarantine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-ingest-quarantine.js","sourceRoot":"","sources":["../../src/commands/codex-ingest-quarantine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { type CodexIngestionConsentDecision, type CodexIngestionConsentState } from '@principles/host-runtime';
2
+ export interface CodexSetupOptions {
3
+ workspace?: string;
4
+ json?: boolean;
5
+ lang?: string;
6
+ accept?: boolean;
7
+ decline?: boolean;
8
+ showDisclosure?: boolean;
9
+ }
10
+ export interface CodexSetupReport {
11
+ generatedAt: string;
12
+ host: 'codex';
13
+ workspace: string;
14
+ status: 'ok' | 'degraded';
15
+ decision?: CodexIngestionConsentDecision;
16
+ consentStateBefore: CodexIngestionConsentState;
17
+ consentState: CodexIngestionConsentState;
18
+ disclosureVersion: string;
19
+ ingestionFlag: {
20
+ name: 'codex_conversation_ingestion';
21
+ enabled: boolean;
22
+ source: string;
23
+ };
24
+ hostCodexFlagEnabled: boolean;
25
+ warnings: string[];
26
+ reason?: string;
27
+ nextAction?: string;
28
+ }
29
+ type FlagWriteResult = {
30
+ ok: true;
31
+ enabled: boolean;
32
+ } | {
33
+ ok: false;
34
+ reason: string;
35
+ nextAction: string;
36
+ };
37
+ /**
38
+ * Enable/disable `features.codex_conversation_ingestion.enabled` in the
39
+ * workspace config.yaml with a line-targeted edit that preserves all other
40
+ * content (comments included). Atomic write; round-trip verified with the
41
+ * production loader, restored on any mismatch.
42
+ */
43
+ export declare function setCodexConversationIngestionFlag(workspaceDir: string, enabled: boolean): FlagWriteResult;
44
+ export declare function handleCodexSetup(options: CodexSetupOptions): Promise<void>;
45
+ export {};
46
+ //# sourceMappingURL=codex-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-setup.d.ts","sourceRoot":"","sources":["../../src/commands/codex-setup.ts"],"names":[],"mappings":"AAoCA,OAAO,EAQL,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAEhC,MAAM,0BAA0B,CAAC;AAGlC,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,UAAU,CAAC;IAC1B,QAAQ,CAAC,EAAE,6BAA6B,CAAC;IACzC,kBAAkB,EAAE,0BAA0B,CAAC;IAC/C,YAAY,EAAE,0BAA0B,CAAC;IACzC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE;QAAE,IAAI,EAAE,8BAA8B,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1F,oBAAoB,EAAE,OAAO,CAAC;IAC9B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,KAAK,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAuB1G;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,eAAe,CAyIzG;AA2BD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6MhF"}