create-principles-disciple 1.144.19 → 1.144.22

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 (154) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/codex-adapter/dist/codex-evidence-recorder.d.ts +56 -0
  3. package/codex-adapter/dist/codex-evidence-recorder.js +235 -0
  4. package/codex-adapter/dist/pd-hook.d.ts +6 -0
  5. package/codex-adapter/dist/pd-hook.js +96 -13
  6. package/codex-adapter/package.json +1 -1
  7. package/console/dist/server/index.js +18 -0
  8. package/console/dist/server/models/ActivationsConsoleModel.d.ts +13 -1
  9. package/console/dist/server/models/ActivationsConsoleModel.js +54 -2
  10. package/console/dist/server/models/ApprovalsConsoleModel.d.ts +98 -19
  11. package/console/dist/server/models/ApprovalsConsoleModel.js +338 -29
  12. package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +17 -7
  13. package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +134 -54
  14. package/console/dist/server/models/ReceiptsConsoleModel.d.ts +44 -1
  15. package/console/dist/server/models/ReceiptsConsoleModel.js +102 -0
  16. package/console/dist/server/routes/activations.js +96 -1
  17. package/console/dist/server/routes/approvals-grouped.js +15 -2
  18. package/console/dist/server/routes/approvals.js +40 -2
  19. package/console/dist/server/routes/evidence-outcomes.d.ts +29 -0
  20. package/console/dist/server/routes/evidence-outcomes.js +137 -0
  21. package/console/dist/server/routes/receipts.js +56 -0
  22. package/console/dist/shared/prompt-injection-contract.d.ts +71 -0
  23. package/console/dist/ui/api.d.ts +94 -4
  24. package/console/dist/ui/api.js +277 -8
  25. package/console/dist/ui/i18n/en.json +113 -13
  26. package/console/dist/ui/i18n/zh-CN.json +113 -13
  27. package/console/dist/ui/pages/activation/ActivationPage.js +80 -2
  28. package/console/dist/ui/pages/focus/FocusPage.js +49 -21
  29. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +75 -10
  30. package/console/dist/ui/utils/approval-warning-localization.js +39 -0
  31. package/console/dist/ui/utils/validators.d.ts +10 -1
  32. package/console/dist/ui/utils/validators.js +126 -0
  33. package/console/dist/web/assets/app.css +15 -0
  34. package/console/dist/web/assets/app.js +29 -28
  35. package/core/dist/runtime-v2/activation/activation-dispatcher.d.ts +4 -0
  36. package/core/dist/runtime-v2/activation/activation-dispatcher.d.ts.map +1 -1
  37. package/core/dist/runtime-v2/activation/activation-dispatcher.js +215 -63
  38. package/core/dist/runtime-v2/activation/activation-dispatcher.js.map +1 -1
  39. package/core/dist/runtime-v2/activation/activation-types.d.ts +48 -0
  40. package/core/dist/runtime-v2/activation/activation-types.d.ts.map +1 -1
  41. package/core/dist/runtime-v2/activation/activation-types.js.map +1 -1
  42. package/core/dist/runtime-v2/activation/approval-completion-service.d.ts +9 -0
  43. package/core/dist/runtime-v2/activation/approval-completion-service.d.ts.map +1 -1
  44. package/core/dist/runtime-v2/activation/approval-completion-service.js +4 -1
  45. package/core/dist/runtime-v2/activation/approval-completion-service.js.map +1 -1
  46. package/core/dist/runtime-v2/activation/index.d.ts +4 -2
  47. package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
  48. package/core/dist/runtime-v2/activation/index.js +4 -1
  49. package/core/dist/runtime-v2/activation/index.js.map +1 -1
  50. package/core/dist/runtime-v2/activation/low-risk-writers.d.ts.map +1 -1
  51. package/core/dist/runtime-v2/activation/low-risk-writers.js +2 -1
  52. package/core/dist/runtime-v2/activation/low-risk-writers.js.map +1 -1
  53. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +11 -0
  54. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
  55. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js +12 -2
  56. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
  57. package/core/dist/runtime-v2/activation/prompt-replacement.d.ts +79 -0
  58. package/core/dist/runtime-v2/activation/prompt-replacement.d.ts.map +1 -0
  59. package/core/dist/runtime-v2/activation/prompt-replacement.js +171 -0
  60. package/core/dist/runtime-v2/activation/prompt-replacement.js.map +1 -0
  61. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.d.ts +17 -1
  62. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.d.ts.map +1 -1
  63. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.js +132 -0
  64. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.js.map +1 -1
  65. package/core/dist/runtime-v2/candidate-audit.d.ts +19 -0
  66. package/core/dist/runtime-v2/candidate-audit.d.ts.map +1 -1
  67. package/core/dist/runtime-v2/candidate-audit.js +72 -12
  68. package/core/dist/runtime-v2/candidate-audit.js.map +1 -1
  69. package/core/dist/runtime-v2/index.d.ts +8 -2
  70. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  71. package/core/dist/runtime-v2/index.js +8 -2
  72. package/core/dist/runtime-v2/index.js.map +1 -1
  73. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +13 -1
  74. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
  75. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js +15 -2
  76. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
  77. package/core/dist/runtime-v2/intervention-evidence-normalizer.d.ts +28 -0
  78. package/core/dist/runtime-v2/intervention-evidence-normalizer.d.ts.map +1 -0
  79. package/core/dist/runtime-v2/intervention-evidence-normalizer.js +654 -0
  80. package/core/dist/runtime-v2/intervention-evidence-normalizer.js.map +1 -0
  81. package/core/dist/runtime-v2/pain-chain-read-model.d.ts.map +1 -1
  82. package/core/dist/runtime-v2/pain-chain-read-model.js +48 -14
  83. package/core/dist/runtime-v2/pain-chain-read-model.js.map +1 -1
  84. package/core/dist/runtime-v2/receipt-coverage.d.ts +8 -0
  85. package/core/dist/runtime-v2/receipt-coverage.d.ts.map +1 -1
  86. package/core/dist/runtime-v2/receipt-coverage.js +18 -0
  87. package/core/dist/runtime-v2/receipt-coverage.js.map +1 -1
  88. package/core/dist/runtime-v2/store/intervention-evidence-schema.d.ts +6 -0
  89. package/core/dist/runtime-v2/store/intervention-evidence-schema.d.ts.map +1 -0
  90. package/core/dist/runtime-v2/store/intervention-evidence-schema.js +122 -0
  91. package/core/dist/runtime-v2/store/intervention-evidence-schema.js.map +1 -0
  92. package/core/dist/runtime-v2/store/sqlite-connection.d.ts +8 -0
  93. package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  94. package/core/dist/runtime-v2/store/sqlite-connection.js +62 -3
  95. package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  96. package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.d.ts +67 -0
  97. package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.d.ts.map +1 -0
  98. package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.js +737 -0
  99. package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.js.map +1 -0
  100. package/core/dist/runtime-v2/types/event-types.d.ts +6 -0
  101. package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
  102. package/core/dist/runtime-v2/types/event-types.js +2 -0
  103. package/core/dist/runtime-v2/types/event-types.js.map +1 -1
  104. package/core/dist/runtime-v2/types/intervention-evidence-contract.d.ts +287 -0
  105. package/core/dist/runtime-v2/types/intervention-evidence-contract.d.ts.map +1 -0
  106. package/core/dist/runtime-v2/types/intervention-evidence-contract.js +84 -0
  107. package/core/dist/runtime-v2/types/intervention-evidence-contract.js.map +1 -0
  108. package/core/package.json +5 -1
  109. package/host-runtime/dist/active-principle-prompt.d.ts +2 -0
  110. package/host-runtime/dist/active-principle-prompt.js +1 -1
  111. package/host-runtime/dist/host-liveness-contract.d.ts +27 -0
  112. package/host-runtime/dist/host-liveness-contract.js +38 -12
  113. package/host-runtime/dist/index.d.ts +4 -0
  114. package/host-runtime/dist/index.js +16 -0
  115. package/host-runtime/dist/intervention-evidence-ingress.d.ts +41 -0
  116. package/host-runtime/dist/intervention-evidence-ingress.js +272 -0
  117. package/host-runtime/dist/prompt-activation-deliverability.d.ts +106 -0
  118. package/host-runtime/dist/prompt-activation-deliverability.js +176 -0
  119. package/host-runtime/dist/prompt-injection-event-evidence.d.ts +51 -0
  120. package/host-runtime/dist/prompt-injection-event-evidence.js +141 -0
  121. package/host-runtime/dist/prompt-injection-projection.d.ts +135 -19
  122. package/host-runtime/dist/prompt-injection-projection.js +224 -20
  123. package/host-runtime/dist/prompt-injection-status.d.ts +44 -0
  124. package/host-runtime/dist/prompt-injection-status.js +147 -0
  125. package/host-runtime/package.json +1 -1
  126. package/package.json +2 -2
  127. package/pd-cli/dist/commands/build-trajectory-evidence.d.ts +21 -0
  128. package/pd-cli/dist/commands/build-trajectory-evidence.d.ts.map +1 -1
  129. package/pd-cli/dist/commands/build-trajectory-evidence.js +136 -24
  130. package/pd-cli/dist/commands/build-trajectory-evidence.js.map +1 -1
  131. package/pd-cli/dist/commands/candidate.d.ts +6 -4
  132. package/pd-cli/dist/commands/candidate.d.ts.map +1 -1
  133. package/pd-cli/dist/commands/candidate.js +144 -32
  134. package/pd-cli/dist/commands/candidate.js.map +1 -1
  135. package/pd-cli/dist/commands/pain-record.d.ts.map +1 -1
  136. package/pd-cli/dist/commands/pain-record.js +33 -7
  137. package/pd-cli/dist/commands/pain-record.js.map +1 -1
  138. package/pd-cli/dist/commands/runtime-activation.d.ts +4 -0
  139. package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
  140. package/pd-cli/dist/commands/runtime-activation.js +148 -23
  141. package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
  142. package/pd-cli/dist/services/reuse-gated-intake.d.ts +14 -0
  143. package/pd-cli/dist/services/reuse-gated-intake.d.ts.map +1 -0
  144. package/pd-cli/dist/services/reuse-gated-intake.js +53 -0
  145. package/pd-cli/dist/services/reuse-gated-intake.js.map +1 -0
  146. package/pd-cli/package.json +1 -1
  147. package/plugin/dist/bundle.js +733 -568
  148. package/plugin/dist/core/intervention-evidence-recorder.d.ts +65 -0
  149. package/plugin/dist/governance-audit.js +2 -2
  150. package/plugin/dist/hooks/gate.d.ts +3 -0
  151. package/plugin/dist/openclaw.plugin.json +1 -1
  152. package/plugin/openclaw.plugin.json +1 -1
  153. package/plugin/package.json +1 -1
  154. package/release-manager/package.json +2 -2
@@ -1,11 +1,42 @@
1
1
  import type { ApprovalListFilter, ApprovalListResult, ApprovalDecisionResult, ApprovalRecord, ApprovalStatus } from '@principles/core/runtime-v2';
2
2
  import type { ApprovalWithContext, ActivationDecision } from '@principles/core/runtime-v2';
3
+ import { type PromptInjectionTargetHost } from '@principles/host-runtime';
3
4
  type UnsupportedChannelResult = {
4
5
  ok: false;
5
6
  error: 'unsupported_channel';
6
7
  channel: string;
7
8
  };
8
9
  type ChannelGuardedDecisionResult = ApprovalDecisionResult | UnsupportedChannelResult;
10
+ /** R-B3 propose-revision result: reviewable diff + queued approval, or a structured refusal. */
11
+ export type ProposePromptRevisionResult = {
12
+ ok: true;
13
+ /** true = an equivalent pending revision approval already existed (idempotent replay). */
14
+ alreadyPending: boolean;
15
+ newArtifactId: string;
16
+ approvalId: string;
17
+ supersededActivationId: string;
18
+ oldStatement: string;
19
+ newStatement: string;
20
+ title: string;
21
+ diff: {
22
+ intentContract?: unknown;
23
+ note?: string;
24
+ };
25
+ deliverability?: {
26
+ status: string;
27
+ reason?: string;
28
+ costChars?: number;
29
+ budget?: number;
30
+ route?: string;
31
+ };
32
+ } | {
33
+ ok: false;
34
+ error: 'not_found' | 'activation_not_live_prompt' | 'artifact_unavailable' | 'revision_validation_failed' | 'revision_enqueue_failed';
35
+ reason?: string;
36
+ nextAction?: string;
37
+ activationId?: string;
38
+ artifactId?: string;
39
+ };
9
40
  export type ApproveWithActivationResult = {
10
41
  ok: true;
11
42
  record: ApprovalRecord;
@@ -27,6 +58,36 @@ export type ApproveWithActivationResult = {
27
58
  error: 'activation_failed';
28
59
  reason: string;
29
60
  approvalRolledBack: boolean;
61
+ nextAction?: string;
62
+ } | {
63
+ ok: false;
64
+ error: 'prompt_replacement_refused';
65
+ reason: string;
66
+ nextAction: string;
67
+ }
68
+ /**
69
+ * PD_PROMPT_CAPACITY_V1 R-B2: the prompt-channel write gate refused BEFORE
70
+ * any governance write. The approval stays pending, the artifact stays
71
+ * untouched, no live activation and no ledger upgrade happens.
72
+ */
73
+ | {
74
+ ok: false;
75
+ error: 'prompt_capacity_refused';
76
+ reason: string;
77
+ nextAction: string;
78
+ capacity: {
79
+ category: 'single_item_exceeds_budget' | 'route_unconfirmed' | 'content_unconfirmed';
80
+ route?: 'legacy_trim' | 'shared_render';
81
+ hostKind?: 'openclaw' | 'codex';
82
+ budget?: number;
83
+ costChars?: number;
84
+ overByChars?: number;
85
+ perHost?: {
86
+ hostKind: 'openclaw' | 'codex';
87
+ fits: boolean;
88
+ costChars: number;
89
+ }[];
90
+ };
30
91
  };
31
92
  export type ReopenApprovalResult = {
32
93
  ok: true;
@@ -53,28 +114,31 @@ export declare class ApprovalsConsoleModel {
53
114
  getApprovalDetail(approvalId: string): Promise<(ApprovalWithContext & {
54
115
  isMvpProven: boolean;
55
116
  }) | null>;
56
- approve(approvalId: string, decidedBy: string, note?: string): Promise<ApproveWithActivationResult>;
117
+ approve(approvalId: string, decidedBy: string, options?: {
118
+ note?: string;
119
+ targetHost?: PromptInjectionTargetHost;
120
+ intentReviewed?: boolean;
121
+ reviewedArtifactId?: string;
122
+ retryActivation?: boolean;
123
+ }): Promise<ApproveWithActivationResult>;
124
+ /**
125
+ * PD_PROMPT_CAPACITY_V1 R-B2: run the shared host-runtime deliverability
126
+ * precheck for a prompt-channel approval artifact. Returns the refusal
127
+ * result when the single item cannot be delivered or the facts cannot be
128
+ * confirmed; returns undefined when the write may proceed.
129
+ */
130
+ private precheckPromptCapacity;
57
131
  /**
58
132
  * PRI-890 (PRI-768 v6-02): recompute the production prompt injection
59
133
  * projection and report whether `activationId` made it into the injected
60
- * set. PR #1844 follow-up: the projection comes from
61
- * `buildLivePromptInjectionProjection`, which follows the SAME route the
62
- * real agent injection takes (legacy trimToBudget vs shared render, chosen
63
- * by the abstraction_layer_v1 flag) instead of always forecasting with the
64
- * shared-path renderer. Budget exclusion is a warning, never a failure —
65
- * the activation is committed and the Owner decides whether to retire older
66
- * principles. A projection error is also surfaced (never silent) but must
67
- * not fail the approve.
68
- *
69
- * PR-1894 — the console holds no session, so it cannot supply the production
70
- * round key and the projection runs `legacy_fifo_prefix_v1`. That is NOT a
71
- * conservative forecast: FIFO structurally drops the newest activation from
72
- * every truncated selection, while the live plugin route rotates and injects
73
- * it within N user turns (PRI-904). Reporting that as "will NOT enter agent
74
- * behavior until older ones are deactivated" told the Owner a falsehood and
75
- * pushed them to deactivate healthy principles. The console now reports the
76
- * policy it actually ran and, when the activation is merely rotated out of
77
- * the current window, says so instead of claiming starvation.
134
+ * set. PD_PROMPT_CAPACITY_V1: the projection follows the host-fact-bound
135
+ * route (resolveLivePromptInjectionProjection), an oversized activation is
136
+ * explained as OVERSIZED rather than folded into generic budget exclusion
137
+ * (AC-02), and an unconfirmed route yields an explicit
138
+ * `injection_capacity_unconfirmed` warning instead of a guessed FIFO or
139
+ * rotation story (AC-01). Budget exclusion remains a warning, never a
140
+ * failure — the activation is committed and the Owner decides whether to
141
+ * retire older principles.
78
142
  */
79
143
  private checkPromptInjectionBudget;
80
144
  /**
@@ -141,6 +205,21 @@ export declare class ApprovalsConsoleModel {
141
205
  private readSafeGetById;
142
206
  /** Returns null when the approvals table does not exist. */
143
207
  private readSafeList;
208
+ /**
209
+ * PD_PROMPT_CAPACITY_V1 R-B3: Owner-initiated "modify to injectable version"
210
+ * entry on a LIVE prompt activation. Produces a NEW artifact version (the
211
+ * old approved artifact stays immutable), re-validates through the real
212
+ * scribe content contract, enqueues a NORMAL pending approval for it (the
213
+ * Owner reviews the diff + intent fields on the focus page and approves —
214
+ * the approve path then performs the atomic replacement). Duplicate requests
215
+ * are idempotent: an existing pending revision approval is returned as-is.
216
+ */
217
+ proposePromptRevision(input: {
218
+ activationId: string;
219
+ statement: string;
220
+ editedBy: string;
221
+ targetHost?: PromptInjectionTargetHost;
222
+ }): Promise<ProposePromptRevisionResult>;
144
223
  dispose(): void;
145
224
  }
146
225
  export {};
@@ -1,9 +1,9 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { SqliteConnection, SqliteApprovalQueueStore, SqliteActivationStateStore, SqlitePIArtifactStore, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, ApprovalCompletionService, ApprovalQueue, mapConfidenceToLabel, isArtifactRevisionOf, PrincipleTreeLedgerAdapter, MVP_CHANNELS, } from '@principles/core/runtime-v2';
3
+ import { SqliteConnection, SqliteApprovalQueueStore, SqliteActivationStateStore, SqlitePIArtifactStore, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, ApprovalCompletionService, ApprovalQueue, mapConfidenceToLabel, isArtifactRevisionOf, detectPromptReplacementTarget, buildOwnerRevisionArtifact, PrincipleTreeLedgerAdapter, MVP_CHANNELS, } from '@principles/core/runtime-v2';
4
4
  import { resolveLedgerPrincipleId } from './principle-id-resolution.js';
5
5
  import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
6
- import { resolveWorkspaceHostToolSemantics, buildLivePromptInjectionProjection } from '@principles/host-runtime';
6
+ import { resolveWorkspaceHostToolSemantics, checkPromptArtifactDeliverabilityById, resolveLivePromptInjectionProjection, } from '@principles/host-runtime';
7
7
  const MVP_PROVEN_CHANNELS = new Set(MVP_CHANNELS);
8
8
  const EMPTY_STATS = { pending: 0, approved: 0, rejected: 0, cancelled: 0 };
9
9
  function isMissingTableError(err) {
@@ -75,7 +75,9 @@ export class ApprovalsConsoleModel {
75
75
  isMvpProven: MVP_PROVEN_CHANNELS.has(record.channel),
76
76
  };
77
77
  }
78
- async approve(approvalId, decidedBy, note) {
78
+ async approve(approvalId, decidedBy, options) {
79
+ const note = options?.note;
80
+ const targetHost = options?.targetHost;
79
81
  if (!stateDbExists(this.workspaceDir)) {
80
82
  return { ok: false, error: 'not_found' };
81
83
  }
@@ -85,10 +87,58 @@ export class ApprovalsConsoleModel {
85
87
  if (!MVP_PROVEN_CHANNELS.has(existing.channel)) {
86
88
  return { ok: false, error: 'unsupported_channel', channel: existing.channel };
87
89
  }
90
+ // PD_PROMPT_CAPACITY_V1 R-B2/R-B3: for a PENDING prompt approval —
91
+ // (1) the route-aware single-artifact capacity precheck BEFORE any
92
+ // governance write (re-reads the CURRENT artifact and route/config at
93
+ // submit time — a stale preview never authorizes the write, AC-08;
94
+ // refusal keeps the approval pending, the artifact, no activation, no
95
+ // ledger upgrade), then (2) replacement detection: approving an artifact
96
+ // that is a REVISION of a live prompt activation means version
97
+ // replacement — pass the single supersede target through so the
98
+ // activation commit replaces the old version atomically (detection fails
99
+ // closed on ambiguity). Already-decided rows reach their terminal-state
100
+ // error (409) untouched by both gates.
101
+ let supersedeActivationId;
102
+ let supersedeArtifactId;
103
+ if (existing.channel === 'prompt' && (existing.status === 'pending' || (existing.status === 'approved' && options?.retryActivation === true))) {
104
+ const precheck = await this.precheckPromptCapacity(existing.artifactId, targetHost);
105
+ if (precheck !== undefined)
106
+ return precheck;
107
+ const { connection } = this.createReadContext();
108
+ try {
109
+ const piArtifactStore = new SqlitePIArtifactStore(connection);
110
+ const detection = await detectPromptReplacementTarget({
111
+ approvalArtifactId: existing.artifactId,
112
+ getArtifactById: async (id) => piArtifactStore.getArtifactById(id),
113
+ listPromptActivations: async () => new SqliteActivationStateStore(connection).listPromptActivations(),
114
+ });
115
+ if (!detection.ok) {
116
+ return {
117
+ ok: false,
118
+ error: 'prompt_replacement_refused',
119
+ reason: detection.error,
120
+ nextAction: detection.nextAction,
121
+ };
122
+ }
123
+ supersedeActivationId = detection.target?.supersededActivationId;
124
+ supersedeArtifactId = detection.target?.supersededArtifactId;
125
+ if (existing.status === 'pending' && detection.target !== null && (options?.intentReviewed !== true || options.reviewedArtifactId !== existing.artifactId)) {
126
+ return { ok: false, error: 'prompt_replacement_refused', reason: 'revision_intent_review_required', nextAction: 'review the old and new statements and retained intent fields, then confirm intent review on the existing approval action' };
127
+ }
128
+ }
129
+ finally {
130
+ try {
131
+ connection.close();
132
+ }
133
+ catch { /* best-effort */ }
134
+ }
135
+ }
88
136
  const { queue: writeQueue, connection: writeConnection } = this.createWriteContext();
89
137
  let approvalResult;
90
138
  try {
91
- approvalResult = await writeQueue.approve(approvalId, decidedBy, note);
139
+ approvalResult = existing.channel === 'prompt' && existing.status === 'approved' && options?.retryActivation === true
140
+ ? { ok: true, record: existing }
141
+ : await writeQueue.approve(approvalId, decidedBy, note);
92
142
  }
93
143
  finally {
94
144
  try {
@@ -102,11 +152,15 @@ export class ApprovalsConsoleModel {
102
152
  }
103
153
  return { ok: false, error: 'not_found' };
104
154
  }
105
- const activation = await this.dispatchActivationAfterApproval(existing, decidedBy);
106
- // If activation failed, roll back approval to pending so the user can retry.
155
+ const activation = await this.dispatchActivationAfterApproval(approvalResult.record, approvalResult.record.decidedBy ?? 'unknown', { supersedeActivationId, supersedeArtifactId });
156
+ // Prompt failures retain the Owner decision; other channels preserve their existing rollback contract.
107
157
  if (activation && !isActivationSuccess(activation)) {
108
158
  // eslint-disable-next-line no-restricted-syntax -- 'in' required for discriminated union narrowing (ActivationDecision)
109
159
  const detail = 'reason' in activation ? activation.reason : activation.decision;
160
+ if (existing.channel === 'prompt') {
161
+ return { ok: false, error: 'activation_failed', reason: detail, approvalRolledBack: false,
162
+ nextAction: `Approval remains approved. Retry with pd runtime activation approve --approval-id ${approvalId} --retry-activation${targetHost === undefined ? '' : ` --target-host ${targetHost}`}.` };
163
+ }
110
164
  let approvalRolledBack = false;
111
165
  try {
112
166
  const { queue: rollbackQueue, connection: rollbackConnection } = this.createWriteContext();
@@ -164,47 +218,108 @@ export class ApprovalsConsoleModel {
164
218
  // isActivationSuccess is not a type predicate; the success variants all
165
219
  // carry activationId (activation-types.ts) — narrow with `in` (rc-2).
166
220
  if ('activationId' in activation) {
167
- injectionWarning = await this.checkPromptInjectionBudget(activation.activationId);
221
+ injectionWarning = await this.checkPromptInjectionBudget(activation.activationId, targetHost);
168
222
  }
169
223
  }
170
224
  const warnings = [ledgerWarning, injectionWarning].filter((w) => w !== undefined);
171
225
  return { ok: true, record: approvalResult.record, activation, warning: warnings.length > 0 ? warnings.join('; ') : undefined };
172
226
  }
227
+ /**
228
+ * PD_PROMPT_CAPACITY_V1 R-B2: run the shared host-runtime deliverability
229
+ * precheck for a prompt-channel approval artifact. Returns the refusal
230
+ * result when the single item cannot be delivered or the facts cannot be
231
+ * confirmed; returns undefined when the write may proceed.
232
+ */
233
+ async precheckPromptCapacity(artifactId, targetHost) {
234
+ let deliverability;
235
+ try {
236
+ deliverability = await checkPromptArtifactDeliverabilityById({
237
+ workspaceDir: this.workspaceDir,
238
+ artifactId,
239
+ targetHost,
240
+ });
241
+ }
242
+ catch (error) {
243
+ // rc-9: a crashed precheck is NOT a pass — refuse with the reason.
244
+ const message = error instanceof Error ? error.message : String(error);
245
+ return {
246
+ ok: false,
247
+ error: 'prompt_capacity_refused',
248
+ reason: `prompt_capacity_precheck_failed: ${message}`,
249
+ nextAction: 'check the workspace .pd/state.db readability and retry the approval',
250
+ capacity: { category: 'content_unconfirmed' },
251
+ };
252
+ }
253
+ if (deliverability.status === 'deliverable')
254
+ return undefined;
255
+ if (deliverability.status === 'undeliverable') {
256
+ return {
257
+ ok: false,
258
+ error: 'prompt_capacity_refused',
259
+ reason: `single_item_exceeds_budget: this principle's own serialized entry is ${deliverability.costChars} chars > the ${deliverability.budget}-char budget on the ${deliverability.route === 'shared_render' ? 'shared full-render' : 'list-entry'} route${deliverability.hostKind !== undefined ? ` (host ${deliverability.hostKind})` : ''} — approving it could never inject it`,
260
+ nextAction: deliverability.nextAction,
261
+ capacity: {
262
+ category: 'single_item_exceeds_budget',
263
+ route: deliverability.route,
264
+ ...(deliverability.hostKind !== undefined ? { hostKind: deliverability.hostKind } : {}),
265
+ budget: deliverability.budget,
266
+ costChars: deliverability.costChars,
267
+ overByChars: deliverability.overByChars,
268
+ },
269
+ };
270
+ }
271
+ return {
272
+ ok: false,
273
+ error: 'prompt_capacity_refused',
274
+ reason: deliverability.reason,
275
+ nextAction: deliverability.nextAction,
276
+ capacity: {
277
+ category: deliverability.reason.startsWith('route_unconfirmed') ? 'route_unconfirmed' : 'content_unconfirmed',
278
+ ...(deliverability.perHost !== undefined
279
+ ? { perHost: deliverability.perHost.map((entry) => ({ hostKind: entry.hostKind, fits: entry.fits, costChars: entry.costChars })) }
280
+ : {}),
281
+ },
282
+ };
283
+ }
173
284
  /**
174
285
  * PRI-890 (PRI-768 v6-02): recompute the production prompt injection
175
286
  * projection and report whether `activationId` made it into the injected
176
- * set. PR #1844 follow-up: the projection comes from
177
- * `buildLivePromptInjectionProjection`, which follows the SAME route the
178
- * real agent injection takes (legacy trimToBudget vs shared render, chosen
179
- * by the abstraction_layer_v1 flag) instead of always forecasting with the
180
- * shared-path renderer. Budget exclusion is a warning, never a failure —
181
- * the activation is committed and the Owner decides whether to retire older
182
- * principles. A projection error is also surfaced (never silent) but must
183
- * not fail the approve.
184
- *
185
- * PR-1894 — the console holds no session, so it cannot supply the production
186
- * round key and the projection runs `legacy_fifo_prefix_v1`. That is NOT a
187
- * conservative forecast: FIFO structurally drops the newest activation from
188
- * every truncated selection, while the live plugin route rotates and injects
189
- * it within N user turns (PRI-904). Reporting that as "will NOT enter agent
190
- * behavior until older ones are deactivated" told the Owner a falsehood and
191
- * pushed them to deactivate healthy principles. The console now reports the
192
- * policy it actually ran and, when the activation is merely rotated out of
193
- * the current window, says so instead of claiming starvation.
287
+ * set. PD_PROMPT_CAPACITY_V1: the projection follows the host-fact-bound
288
+ * route (resolveLivePromptInjectionProjection), an oversized activation is
289
+ * explained as OVERSIZED rather than folded into generic budget exclusion
290
+ * (AC-02), and an unconfirmed route yields an explicit
291
+ * `injection_capacity_unconfirmed` warning instead of a guessed FIFO or
292
+ * rotation story (AC-01). Budget exclusion remains a warning, never a
293
+ * failure — the activation is committed and the Owner decides whether to
294
+ * retire older principles.
194
295
  */
195
- async checkPromptInjectionBudget(activationId) {
196
- let projection;
296
+ async checkPromptInjectionBudget(activationId, targetHost) {
297
+ let resolution;
197
298
  try {
198
- projection = await buildLivePromptInjectionProjection({ workspaceDir: this.workspaceDir });
299
+ resolution = await resolveLivePromptInjectionProjection({
300
+ workspaceDir: this.workspaceDir,
301
+ targetHost,
302
+ });
199
303
  }
200
304
  catch (error) {
201
305
  const message = error instanceof Error ? error.message : String(error);
202
306
  return `injection_budget_check_failed: could not recompute the prompt injection projection (${message}); the activation is committed but its injection status is unverified. nextAction=check .pd/state.db readability and re-run pd runtime activation list`;
203
307
  }
308
+ if (resolution.status === 'unconfirmed') {
309
+ return `injection_capacity_unconfirmed: the activation is committed but this workspace's effective injection route cannot be confirmed (${resolution.decision.unconfirmedReason ?? 'unknown reason'}); per-host forecasts: ${resolution.perHost.map((entry) => `${entry.hostKind}=${entry.projection.route} (${entry.projection.usedChars}c${entry.projection.truncated ? ', truncated' : ''})`).join('; ')}. nextAction=${resolution.decision.nextAction ?? 'pass an explicit target host (openclaw|codex)'}`;
310
+ }
311
+ const { projection } = resolution;
204
312
  if (projection.injectedActivationIds.includes(activationId)) {
205
313
  return undefined;
206
314
  }
207
315
  const projectionDetail = projection.warnings.length > 0 ? ` projection warnings: ${projection.warnings.join(' | ')}` : '';
316
+ // AC-02/R-A3: oversized is independent of `truncated` — say WHY first.
317
+ if (projection.oversizedActivationIds.includes(activationId)) {
318
+ const scopeNote = projection.route === 'shared_render'
319
+ ? 'the full directive render of this single principle exceeds the cap'
320
+ : 'its own list entry exceeds the cap';
321
+ return `injection_oversized: the activation is committed but ${scopeNote} (${projection.budget} chars, measured in UTF-16 units on the ${projection.route} route) — it can never enter the prompt as written, regardless of rotation or free capacity. nextAction=shorten the statement via the activations page "modify to injectable version" entry, or deactivate this activation`;
322
+ }
208
323
  if (!projection.truncated) {
209
324
  // Not included and the budget did NOT truncate — the projection skipped
210
325
  // this activation for another reason (e.g. artifact resolution). Report
@@ -378,7 +493,7 @@ export class ApprovalsConsoleModel {
378
493
  catch { /* best-effort */ }
379
494
  }
380
495
  }
381
- async dispatchActivationAfterApproval(existing, decidedBy) {
496
+ async dispatchActivationAfterApproval(existing, decidedBy, replacement) {
382
497
  // Feature flag gate (Contract F): when story_a_approval_completion is disabled,
383
498
  // the new orchestrator is deactivated without damaging existing data.
384
499
  // The approval remains in 'approved' status; only activation is skipped.
@@ -482,6 +597,7 @@ export class ApprovalsConsoleModel {
482
597
  approvalId: existing.approvalId,
483
598
  actor: { kind: 'human', userId: decidedBy },
484
599
  now: new Date().toISOString(),
600
+ ...(replacement?.supersedeActivationId !== undefined ? { ...replacement, supersedeDecidedBy: decidedBy } : {}),
485
601
  });
486
602
  if (!completionResult.ok) {
487
603
  return {
@@ -617,6 +733,199 @@ export class ApprovalsConsoleModel {
617
733
  catch { /* best-effort */ }
618
734
  }
619
735
  }
736
+ /**
737
+ * PD_PROMPT_CAPACITY_V1 R-B3: Owner-initiated "modify to injectable version"
738
+ * entry on a LIVE prompt activation. Produces a NEW artifact version (the
739
+ * old approved artifact stays immutable), re-validates through the real
740
+ * scribe content contract, enqueues a NORMAL pending approval for it (the
741
+ * Owner reviews the diff + intent fields on the focus page and approves —
742
+ * the approve path then performs the atomic replacement). Duplicate requests
743
+ * are idempotent: an existing pending revision approval is returned as-is.
744
+ */
745
+ async proposePromptRevision(input) {
746
+ if (!stateDbExists(this.workspaceDir)) {
747
+ return { ok: false, error: 'not_found' };
748
+ }
749
+ const now = new Date().toISOString();
750
+ const { connection } = this.createWriteContext();
751
+ try {
752
+ const activationStore = new SqliteActivationStateStore(connection);
753
+ const artifactStore = new SqlitePIArtifactStore(connection);
754
+ const livePrompt = (await activationStore.listPromptActivations())
755
+ .find((activation) => activation.activationId === input.activationId && activation.deactivatedAt === null);
756
+ if (!livePrompt) {
757
+ return { ok: false, error: 'activation_not_live_prompt', activationId: input.activationId };
758
+ }
759
+ const oldArtifact = await artifactStore.getArtifactById(livePrompt.artifactId);
760
+ if (!oldArtifact) {
761
+ return { ok: false, error: 'artifact_unavailable', artifactId: livePrompt.artifactId };
762
+ }
763
+ const oldSnapshot = {
764
+ artifactId: oldArtifact.artifactId,
765
+ artifactKind: oldArtifact.artifactKind,
766
+ sourceTaskId: oldArtifact.sourceTaskId,
767
+ ...(oldArtifact.sourcePrincipleId !== null && oldArtifact.sourcePrincipleId !== undefined ? { sourcePrincipleId: oldArtifact.sourcePrincipleId } : {}),
768
+ ...(oldArtifact.sourceRuleId !== null && oldArtifact.sourceRuleId !== undefined ? { sourceRuleId: oldArtifact.sourceRuleId } : {}),
769
+ lineageArtifactIds: oldArtifact.lineageArtifactIds,
770
+ validationStatus: oldArtifact.validationStatus,
771
+ contentJson: oldArtifact.contentJson,
772
+ createdAt: oldArtifact.createdAt,
773
+ updatedAt: oldArtifact.updatedAt,
774
+ };
775
+ // Idempotency: a PENDING approval for another artifact version of this
776
+ // same principle already exists → return it (no second artifact, no
777
+ // second approval). Rejected/approved history does not block a new try.
778
+ const queueStore = new SqliteApprovalQueueStore(connection);
779
+ const pendingApprovals = (await new ApprovalQueue(queueStore).listAll({ status: 'pending', channel: 'prompt' }));
780
+ for (const pending of pendingApprovals) {
781
+ if (pending.artifactId === oldSnapshot.artifactId)
782
+ continue;
783
+ const pendingArtifact = await artifactStore.getArtifactById(pending.artifactId);
784
+ if (!pendingArtifact)
785
+ continue;
786
+ const pendingSnapshot = {
787
+ artifactId: pendingArtifact.artifactId,
788
+ artifactKind: pendingArtifact.artifactKind,
789
+ sourceTaskId: pendingArtifact.sourceTaskId,
790
+ ...(pendingArtifact.sourcePrincipleId !== null && pendingArtifact.sourcePrincipleId !== undefined ? { sourcePrincipleId: pendingArtifact.sourcePrincipleId } : {}),
791
+ ...(pendingArtifact.sourceRuleId !== null && pendingArtifact.sourceRuleId !== undefined ? { sourceRuleId: pendingArtifact.sourceRuleId } : {}),
792
+ lineageArtifactIds: pendingArtifact.lineageArtifactIds,
793
+ validationStatus: pendingArtifact.validationStatus,
794
+ contentJson: pendingArtifact.contentJson,
795
+ createdAt: pendingArtifact.createdAt,
796
+ updatedAt: pendingArtifact.updatedAt,
797
+ };
798
+ if (isArtifactRevisionOf(pendingSnapshot, oldSnapshot)) {
799
+ return {
800
+ ok: true,
801
+ alreadyPending: true,
802
+ newArtifactId: pending.artifactId,
803
+ approvalId: pending.approvalId,
804
+ supersededActivationId: livePrompt.activationId,
805
+ oldStatement: '',
806
+ newStatement: '',
807
+ title: '',
808
+ diff: { note: 'a pending revision approval already exists for this principle' },
809
+ };
810
+ }
811
+ }
812
+ // Build + validate the new version through the REAL content contract.
813
+ const build = await buildOwnerRevisionArtifact({
814
+ oldArtifact: oldSnapshot,
815
+ newStatement: input.statement,
816
+ editedBy: input.editedBy,
817
+ now,
818
+ });
819
+ if (!build.ok) {
820
+ return { ok: false, error: 'revision_validation_failed', reason: build.error, nextAction: build.nextAction };
821
+ }
822
+ await artifactStore.upsertArtifact({
823
+ artifactId: build.draft.artifactId,
824
+ artifactKind: 'principle',
825
+ sourceTaskId: build.draft.sourceTaskId,
826
+ ...(build.draft.sourcePrincipleId !== undefined ? { sourcePrincipleId: build.draft.sourcePrincipleId } : {}),
827
+ sourceRuleId: undefined,
828
+ lineageArtifactIds: build.draft.lineageArtifactIds,
829
+ validationStatus: 'pending',
830
+ contentJson: build.draft.contentJson,
831
+ createdAt: now,
832
+ updatedAt: now,
833
+ });
834
+ const validated = await artifactStore.updateValidationStatus(build.draft.artifactId, 'validated');
835
+ if (!validated) {
836
+ return { ok: false, error: 'revision_validation_failed', reason: 'updateValidationStatus returned false', nextAction: 'check pi_artifacts store integrity' };
837
+ }
838
+ // Enqueue the NORMAL approval through the dispatcher (identity gate +
839
+ // writer canActivate — same authority as every other prompt approval).
840
+ const dispatcher = new ActivationDispatcher({
841
+ getArtifactById: async (id) => {
842
+ const rec = await artifactStore.getArtifactById(id);
843
+ if (!rec)
844
+ return null;
845
+ return {
846
+ artifactId: rec.artifactId,
847
+ artifactKind: rec.artifactKind,
848
+ sourceTaskId: rec.sourceTaskId,
849
+ ...(rec.sourcePrincipleId !== null && rec.sourcePrincipleId !== undefined ? { sourcePrincipleId: rec.sourcePrincipleId } : {}),
850
+ ...(rec.sourceRuleId !== null && rec.sourceRuleId !== undefined ? { sourceRuleId: rec.sourceRuleId } : {}),
851
+ lineageArtifactIds: rec.lineageArtifactIds,
852
+ validationStatus: rec.validationStatus,
853
+ contentJson: rec.contentJson,
854
+ createdAt: rec.createdAt,
855
+ updatedAt: rec.updatedAt,
856
+ };
857
+ },
858
+ }, activationStore, {
859
+ writers: [new PromptWriter()],
860
+ approvalQueueStore: queueStore,
861
+ ledgerIdentity: {
862
+ ledger: new PrincipleTreeLedgerAdapter({ stateDir: `${this.workspaceDir}/.state` }),
863
+ getArtifactById: async (id) => {
864
+ const rec = await artifactStore.getArtifactById(id);
865
+ return rec
866
+ ? {
867
+ artifactId: rec.artifactId,
868
+ artifactKind: rec.artifactKind,
869
+ sourceTaskId: rec.sourceTaskId,
870
+ ...(rec.sourcePrincipleId !== null && rec.sourcePrincipleId !== undefined ? { sourcePrincipleId: rec.sourcePrincipleId } : {}),
871
+ ...(rec.sourceRuleId !== null && rec.sourceRuleId !== undefined ? { sourceRuleId: rec.sourceRuleId } : {}),
872
+ lineageArtifactIds: rec.lineageArtifactIds,
873
+ validationStatus: rec.validationStatus,
874
+ contentJson: rec.contentJson,
875
+ createdAt: rec.createdAt,
876
+ updatedAt: rec.updatedAt,
877
+ }
878
+ : null;
879
+ },
880
+ },
881
+ });
882
+ const enqueued = await dispatcher.dispatch({
883
+ artifactId: build.draft.artifactId,
884
+ channel: 'prompt',
885
+ rolloutDecision: 'require_approval',
886
+ actor: { kind: 'human', userId: input.editedBy },
887
+ now,
888
+ confirm: true,
889
+ });
890
+ if (enqueued.decision !== 'queued_for_approval') {
891
+ // rc-9: the artifact stays (pending approval never happened); report why.
892
+ const reason = 'reason' in enqueued ? enqueued.reason : enqueued.decision;
893
+ return { ok: false, error: 'revision_enqueue_failed', reason: String(reason), nextAction: 'check the ledger membership of this principle, then retry' };
894
+ }
895
+ // Route-aware capacity prediction for the NEW statement (review aid).
896
+ let deliverability;
897
+ try {
898
+ deliverability = await checkPromptArtifactDeliverabilityById({
899
+ workspaceDir: this.workspaceDir,
900
+ artifactId: build.draft.artifactId,
901
+ ...(input.targetHost !== undefined ? { targetHost: input.targetHost } : {}),
902
+ });
903
+ }
904
+ catch {
905
+ deliverability = undefined;
906
+ }
907
+ return {
908
+ ok: true,
909
+ alreadyPending: false,
910
+ newArtifactId: build.draft.artifactId,
911
+ approvalId: enqueued.approvalId,
912
+ supersededActivationId: livePrompt.activationId,
913
+ oldStatement: build.oldStatement,
914
+ newStatement: input.statement.trim(),
915
+ title: build.title,
916
+ diff: {
917
+ intentContract: build.intentContract,
918
+ },
919
+ ...(deliverability !== undefined ? { deliverability } : {}),
920
+ };
921
+ }
922
+ finally {
923
+ try {
924
+ connection.close();
925
+ }
926
+ catch { /* best-effort */ }
927
+ }
928
+ }
620
929
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- lifecycle interface; connections are request-scoped
621
930
  dispose() {
622
931
  // Connections are opened and closed per-request; no persistent state.
@@ -1,3 +1,4 @@
1
+ import { type PromptInjectionTargetHost } from '@principles/host-runtime';
1
2
  import type { PromptInjectionBudgetStatus } from '../../shared/prompt-injection-contract.js';
2
3
  export interface ApprovalGroup {
3
4
  principleId: string;
@@ -15,6 +16,12 @@ export interface ApprovalGroup {
15
16
  * (the client has no access to the escaped, serialized size).
16
17
  */
17
18
  fitsPromptBudget?: boolean;
19
+ revisionReviewUnavailable?: boolean;
20
+ revisionReview?: {
21
+ oldStatement: string;
22
+ newStatement: string;
23
+ intentFields: Record<string, string>;
24
+ };
18
25
  /**
19
26
  * PRI-940: the pinned artifact no longer exists in pi_artifacts (typically
20
27
  * superseded by a newer scribe revision). There is no contentJson, so
@@ -42,16 +49,19 @@ export interface ApprovalsGroupedResponse {
42
49
  export declare class ApprovalsGroupedConsoleModel {
43
50
  private readonly workspaceDir;
44
51
  constructor(workspaceDir: string);
45
- getApprovalsGrouped(): Promise<ApprovalsGroupedResponse>;
52
+ getApprovalsGrouped(targetHost?: PromptInjectionTargetHost): Promise<ApprovalsGroupedResponse>;
46
53
  /**
47
54
  * PRI-908: recompute the production prompt injection projection so the
48
- * focus page can forecast "approved �?effective" BEFORE the Owner decides.
55
+ * focus page can forecast "approved → effective" BEFORE the Owner decides.
49
56
  * PR #1844 follow-up: the forecast follows the workspace's REAL injection
50
- * route (`buildLivePromptInjectionProjection` �?legacy trimToBudget vs
51
- * shared render via abstraction_layer_v1), matching the PRI-890 approve-time
52
- * check. Advisory only: a projection failure omits the field �?the
53
- * approve-time warning remains the fail-loud exclusion report (rc-9), so
54
- * this must never fail the grouped read.
57
+ * route; PD_PROMPT_CAPACITY_V1 AC-01 additionally binds that route to host
58
+ * facts (declarations ∪ pain evidence) or an explicit request-level target
59
+ * host — a Codex workspace is never forecast on the list route, and when
60
+ * the applicable hosts disagree the payload says `capacityStatus:
61
+ * 'unconfirmed'` with per-host forecasts instead of guessing. Advisory
62
+ * only: a projection failure omits the field — the approve-time precheck
63
+ * (B1) remains the fail-loud gate (rc-9), so this must never fail the
64
+ * grouped read.
55
65
  */
56
66
  private readPromptInjectionBudgetStatus;
57
67
  dispose(): void;