create-principles-disciple 1.145.12 → 1.145.15

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 (117) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/codex-adapter/package.json +1 -1
  3. package/console/dist/server/models/ActivationsConsoleModel.d.ts +3 -1
  4. package/console/dist/server/models/ActivationsConsoleModel.js +38 -4
  5. package/console/dist/server/models/ApprovalsConsoleModel.js +56 -7
  6. package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +12 -0
  7. package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +70 -13
  8. package/console/dist/server/routes/approvals-grouped.js +5 -2
  9. package/console/dist/shared/prompt-injection-contract.d.ts +58 -5
  10. package/console/dist/ui/i18n/en.json +25 -1
  11. package/console/dist/ui/i18n/zh-CN.json +25 -1
  12. package/console/dist/ui/pages/activation/ActivationPage.js +39 -1
  13. package/console/dist/ui/pages/focus/FocusPage.js +70 -28
  14. package/console/dist/ui/utils/validators.js +70 -11
  15. package/console/dist/web/assets/app.js +24 -24
  16. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
  17. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  18. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts +16 -2
  19. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts.map +1 -1
  20. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js +88 -4
  21. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js.map +1 -1
  22. package/core/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  23. package/core/dist/runtime-v2/internalization/artificer-runner.js +21 -2
  24. package/core/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  25. package/core/package.json +1 -1
  26. package/dist/installers/codex-host-installer.d.ts +1 -1
  27. package/dist/installers/codex-host-installer.js +3 -3
  28. package/dist/installers/codex-host-installer.js.map +1 -1
  29. package/dist/installers/workbuddy-host-installer.js +1 -1
  30. package/dist/installers/workbuddy-host-installer.js.map +1 -1
  31. package/host-runtime/dist/governance-signal-admission.d.ts +16 -2
  32. package/host-runtime/dist/governance-signal-admission.js +27 -7
  33. package/host-runtime/dist/index.d.ts +1 -0
  34. package/host-runtime/dist/index.js +4 -0
  35. package/host-runtime/dist/prompt-injection-budget-observation.d.ts +176 -0
  36. package/host-runtime/dist/prompt-injection-budget-observation.js +449 -0
  37. package/host-runtime/dist/prompt-injection-projection.d.ts +16 -0
  38. package/host-runtime/dist/prompt-injection-projection.js +18 -3
  39. package/host-runtime/dist/prompt-injection-status.d.ts +28 -1
  40. package/host-runtime/dist/prompt-injection-status.js +21 -7
  41. package/host-runtime/package.json +1 -1
  42. package/package.json +2 -2
  43. package/pd-cli/dist/commands/codex-setup.js +1 -1
  44. package/pd-cli/dist/commands/config-doctor.d.ts.map +1 -1
  45. package/pd-cli/dist/commands/config-doctor.js +5 -1
  46. package/pd-cli/dist/commands/config-doctor.js.map +1 -1
  47. package/pd-cli/dist/commands/health-codex-install.d.ts +6 -6
  48. package/pd-cli/dist/commands/health-codex-install.d.ts.map +1 -1
  49. package/pd-cli/dist/commands/health-codex-install.js +7 -7
  50. package/pd-cli/dist/commands/health-codex-install.js.map +1 -1
  51. package/pd-cli/dist/commands/health-codex.js +1 -1
  52. package/pd-cli/dist/commands/health-codex.js.map +1 -1
  53. package/pd-cli/dist/commands/run.d.ts +2 -0
  54. package/pd-cli/dist/commands/run.d.ts.map +1 -1
  55. package/pd-cli/dist/commands/run.js +9 -4
  56. package/pd-cli/dist/commands/run.js.map +1 -1
  57. package/pd-cli/dist/commands/runtime-activation.d.ts +3 -1
  58. package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
  59. package/pd-cli/dist/commands/runtime-activation.js +5 -3
  60. package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
  61. package/pd-cli/dist/index.js +6 -4
  62. package/pd-cli/dist/index.js.map +1 -1
  63. package/pd-cli/dist/services/config-doctor.d.ts +4 -0
  64. package/pd-cli/dist/services/config-doctor.d.ts.map +1 -1
  65. package/pd-cli/dist/services/config-doctor.js +15 -0
  66. package/pd-cli/dist/services/config-doctor.js.map +1 -1
  67. package/pd-cli/package.json +1 -1
  68. package/plugin/dist/bundle.js +391 -391
  69. package/plugin/dist/core/signal-collector-host.d.ts +17 -1
  70. package/plugin/dist/core/signal-health.d.ts +15 -0
  71. package/plugin/dist/openclaw.plugin.json +1 -1
  72. package/plugin/dist/templates/langs/en/skills/pd-cli-operator/SKILL.md +32 -70
  73. package/plugin/dist/templates/langs/en/skills/pd-control/SKILL.md +50 -0
  74. package/plugin/dist/templates/langs/en/skills/pd-pain-signal/SKILL.md +30 -56
  75. package/plugin/dist/templates/langs/en/skills/pd-review/SKILL.md +32 -0
  76. package/plugin/dist/templates/langs/zh/skills/pd-cli-operator/SKILL.md +32 -65
  77. package/plugin/dist/templates/langs/zh/skills/pd-control/SKILL.md +50 -0
  78. package/plugin/dist/templates/langs/zh/skills/pd-pain-signal/SKILL.md +29 -48
  79. package/plugin/dist/templates/langs/zh/skills/pd-review/SKILL.md +32 -0
  80. package/plugin/openclaw.plugin.json +1 -1
  81. package/plugin/package.json +1 -1
  82. package/plugin/templates/langs/en/skills/pd-cli-operator/SKILL.md +32 -70
  83. package/plugin/templates/langs/en/skills/pd-control/SKILL.md +50 -0
  84. package/plugin/templates/langs/en/skills/pd-pain-signal/SKILL.md +30 -56
  85. package/plugin/templates/langs/en/skills/pd-review/SKILL.md +32 -0
  86. package/plugin/templates/langs/zh/skills/pd-cli-operator/SKILL.md +32 -65
  87. package/plugin/templates/langs/zh/skills/pd-control/SKILL.md +50 -0
  88. package/plugin/templates/langs/zh/skills/pd-pain-signal/SKILL.md +29 -48
  89. package/plugin/templates/langs/zh/skills/pd-review/SKILL.md +32 -0
  90. package/release-manager/dist/installers/codex-host-installer.d.ts +1 -1
  91. package/release-manager/dist/installers/codex-host-installer.js +3 -3
  92. package/release-manager/dist/installers/codex-host-installer.js.map +1 -1
  93. package/release-manager/dist/installers/workbuddy-host-installer.js +1 -1
  94. package/release-manager/dist/installers/workbuddy-host-installer.js.map +1 -1
  95. package/release-manager/package.json +2 -2
  96. package/workbuddy-adapter/dist/explicit-owner-statement.js +9 -2
  97. package/workbuddy-adapter/dist/pd-hook.js +5 -2
  98. package/workbuddy-adapter/package.json +1 -1
  99. package/workbuddy-adapter/plugin/skills/pd-cli-operator/SKILL.md +32 -0
  100. package/workbuddy-adapter/plugin/skills/pd-control/SKILL.md +31 -0
  101. package/workbuddy-adapter/plugin/skills/pd-pain-signal/SKILL.md +35 -0
  102. package/workbuddy-adapter/plugin/skills/pd-review/SKILL.md +22 -8
  103. package/plugin/dist/templates/langs/en/skills/pd-implementer/SKILL.md +0 -68
  104. package/plugin/dist/templates/langs/en/skills/pd-mentor/SKILL.md +0 -185
  105. package/plugin/dist/templates/langs/en/skills/pd-runtime-v2/SKILL.md +0 -86
  106. package/plugin/dist/templates/langs/zh/skills/pd-implementer/SKILL.md +0 -68
  107. package/plugin/dist/templates/langs/zh/skills/pd-mentor/SKILL.md +0 -185
  108. package/plugin/dist/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +0 -81
  109. package/plugin/templates/langs/en/skills/pd-implementer/SKILL.md +0 -68
  110. package/plugin/templates/langs/en/skills/pd-mentor/SKILL.md +0 -185
  111. package/plugin/templates/langs/en/skills/pd-runtime-v2/SKILL.md +0 -86
  112. package/plugin/templates/langs/zh/skills/pd-implementer/SKILL.md +0 -68
  113. package/plugin/templates/langs/zh/skills/pd-mentor/SKILL.md +0 -185
  114. package/plugin/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +0 -81
  115. package/workbuddy-adapter/plugin/skills/pd-disable/SKILL.md +0 -18
  116. package/workbuddy-adapter/plugin/skills/pd-record-pain/SKILL.md +0 -21
  117. package/workbuddy-adapter/plugin/skills/pd-status/SKILL.md +0 -17
@@ -0,0 +1,176 @@
1
+ import { PROMPT_BUDGET_POLICY_VERSION, type PromptBudgetCapacitySource, type PromptBudgetCapacityStatus, type PromptBudgetFallbackReason, type PromptBudgetResolution } from '@principles/core/runtime-v2';
2
+ import { type PromptInjectionTargetHost } from './prompt-injection-projection.js';
3
+ /**
4
+ * PRI-1017: runtime budget OBSERVATION read model over the EXISTING
5
+ * injection-event JSONL (`<workspaceDir>/{.pd,.state}/logs/events_<date>.jsonl`,
6
+ * written by the OpenClaw EventLog and the Codex/WorkBuddy shared path via
7
+ * appendEventLogLine).
8
+ *
9
+ * Why this exists: SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1 forbids the
10
+ * actual injection from running a dynamic budget while the Console forecasts
11
+ * under a different fixed 2000-char budget. The console holds no live host
12
+ * facts, but the RUNTIME persists the resolution it actually used —
13
+ * `runtime_v2_prompt_activations_injected` carries budgetChars, capacity
14
+ * provenance and hostKind. This module reads those facts back so console
15
+ * surfaces forecast under the same budget the last real injection used.
16
+ *
17
+ * Honesty rules this model enforces (SPEC §6.1/§7):
18
+ * - An observation is a FACT ABOUT THE PAST, never a live claim: callers must
19
+ * surface `observedAt`; the console labels it "most recent runtime
20
+ * observation", not a current confirmation.
21
+ * - No logs / no budget facts ⇒ absent, never a fabricated window; the caller
22
+ * keeps the honest fixed-fallback policy.
23
+ * - A latest observation older than RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS is
24
+ * `stale` — the caller must NOT present it as the current budget.
25
+ * - Observations bind to ONE host: OpenClaw's window is never used for a
26
+ * Codex or WorkBuddy forecast.
27
+ * - Untrusted input discipline: every line is parsed as `unknown` and
28
+ * narrowed with typeof guards + closed-set checks; structurally invalid
29
+ * lines are counted in `parseFailures` and skipped loudly, never guessed.
30
+ * - Events predating the model-aware policy carry no provenance fields; they
31
+ * are counted as `legacyEventsSkipped` and produce NO observation — a
32
+ * missing provenance field is "policy unknown", never a capacity claim.
33
+ * - Bounded I/O: only the newest bytes of the most recent daily files are read
34
+ * (tail window), the file cap applying PER candidate log directory, so a
35
+ * runaway or crafted log cannot make a Console request read an unbounded
36
+ * amount of data. Only bounded budget metadata is retained and returned;
37
+ * other event payloads are not cached.
38
+ */
39
+ /**
40
+ * Candidate directories for the runtime-v2 event log, in priority order.
41
+ * Shared with the RuleCode telemetry collector (ERR-031: both readers derive
42
+ * from one list — the v1 EventLog writer emits `.state/logs`, no production
43
+ * code ever created `.pd/logs`).
44
+ */
45
+ export declare const RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS: readonly string[];
46
+ /**
47
+ * Bounded I/O: only the most recent daily files are scanned — PER candidate
48
+ * log directory (`RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS`), not globally. The
49
+ * cap is deliberately per directory: both directories describe the same
50
+ * workspace's log convention and either can hold the newest event for a given
51
+ * host (the reader takes latest-wins ACROSS them), so a global cap would let
52
+ * one directory's files starve the other and drop a live host observation.
53
+ * Worst case per read is therefore `RUNTIME_BUDGET_OBSERVATION_MAX_FILES ×
54
+ * RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS.length` files (14 today) and the same
55
+ * count × `RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES` bytes (56 MiB today).
56
+ */
57
+ export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FILES = 7;
58
+ /**
59
+ * Bounded I/O: per file, only the newest bytes are read. The writer appends,
60
+ * so the latest observation per host lives at the TAIL; a file larger than
61
+ * this window is tail-read and its older prefix skipped (counted in
62
+ * `truncatedFiles`) instead of materialising an unbounded log in the console
63
+ * process. 4 MiB is ~3.4x the largest real day observed on 2026-10-21
64
+ * (WorkBuddy 1.17 MiB, Codex 0.87 MiB, OpenClaw 0.98 MiB across 7 files).
65
+ */
66
+ export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES: number;
67
+ /**
68
+ * Maximum age of an observation that may be presented as a CURRENT runtime
69
+ * fact. The runtime refreshes the observation on every prompt build, so an
70
+ * active workspace holds minute-old facts; beyond a day the model may have
71
+ * been switched and the window is no longer evidence about the present.
72
+ */
73
+ export declare const RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS: number;
74
+ /**
75
+ * Clock-skew allowance for event timestamps. A writer clock ahead of ours is
76
+ * normal; an observation further in the future is not a fact about the
77
+ * present and must never be selected as the current budget.
78
+ */
79
+ export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS: number;
80
+ /** Console-side bounded reason: a runtime observation exists but is too old. */
81
+ export declare const RUNTIME_BUDGET_OBSERVATION_STALE_REASON: 'runtime_budget_observation_stale';
82
+ export interface RuntimePromptBudgetObservation {
83
+ /** The workspace the observed event was bound to (event `data.workspaceDir`). */
84
+ workspaceDir: string;
85
+ /** Host that produced the observation; observations never cross hosts. */
86
+ hostKind: PromptInjectionTargetHost;
87
+ /** Budget the real injection ran under (UTF-16 code units). */
88
+ budgetChars: number;
89
+ capacitySource: PromptBudgetCapacitySource;
90
+ capacityStatus: PromptBudgetCapacityStatus;
91
+ /** The context window the budget derived from; null when unknown. */
92
+ contextWindowTokens: number | null;
93
+ /** Bounded fallback reason carried by the event; null when confirmed. */
94
+ fallbackReason: PromptBudgetFallbackReason | null;
95
+ /** Policy version the observed budget was computed under (closed literal). */
96
+ policyVersion: typeof PROMPT_BUDGET_POLICY_VERSION | null;
97
+ /** ISO timestamp of the observed event (`ts` envelope field). */
98
+ observedAt: string;
99
+ }
100
+ export interface RuntimeBudgetObservationReport {
101
+ /** Latest observation per host (most recent by event `ts`). */
102
+ observations: RuntimePromptBudgetObservation[];
103
+ filesRead: string[];
104
+ lines: number;
105
+ parseFailures: number;
106
+ /** Injection events without model-aware budget provenance (pre-policy events). */
107
+ legacyEventsSkipped: number;
108
+ /** Injection events whose budget policy version differs from the current one. */
109
+ foreignPolicyEventsSkipped: number;
110
+ /**
111
+ * Files larger than the bounded read window whose older prefix was skipped.
112
+ * Non-zero means `lines`/skip counters describe the read window only.
113
+ */
114
+ truncatedFiles: number;
115
+ note: string;
116
+ }
117
+ export type RuntimeBudgetObservationSelection = {
118
+ kind: 'usable';
119
+ observation: RuntimePromptBudgetObservation;
120
+ } | {
121
+ kind: 'stale';
122
+ observation: RuntimePromptBudgetObservation;
123
+ } | {
124
+ kind: 'absent';
125
+ };
126
+ /**
127
+ * Read the most recent budget observation per host from the workspace's
128
+ * runtime-v2 event logs. Never throws: an unreadable log directory degrades
129
+ * to an empty report with an explicit note (absence is UNKNOWN, not zero).
130
+ */
131
+ export declare function readRuntimePromptBudgetObservations(input: {
132
+ workspaceDir: string;
133
+ /** Reference clock for the future-timestamp guard (tests pass a fixed one). */
134
+ now?: Date;
135
+ }): RuntimeBudgetObservationReport;
136
+ /**
137
+ * Select the observation that may back a console forecast for `targetHost`.
138
+ * `absent` when the host has no observation at all; `stale` when the latest
139
+ * one is older than the freshness bound (the caller must fall back honestly).
140
+ */
141
+ export declare function selectRuntimeBudgetObservation(input: {
142
+ observations: readonly RuntimePromptBudgetObservation[];
143
+ targetHost?: PromptInjectionTargetHost;
144
+ now?: Date;
145
+ }): RuntimeBudgetObservationSelection;
146
+ /**
147
+ * Console-facing selection: one call gives the confirmed-host budget basis,
148
+ * the honest stale marker, and the per-host bases for unconfirmed routes.
149
+ * Reads the workspace's event logs; never throws.
150
+ */
151
+ export interface ConsoleRuntimeBudgetFacts {
152
+ /** Usable observation for the requested host (only when targetHost is given). */
153
+ selected?: RuntimePromptBudgetObservation;
154
+ /** Latest observation for the requested host that is too old to be current. */
155
+ stale?: RuntimePromptBudgetObservation;
156
+ /**
157
+ * Usable observations per host — used for unconfirmed per-host forecasts
158
+ * AND for a confirmed route whose host was resolved from workspace facts
159
+ * (no request-level targetHost). Each host only ever gets its own fact.
160
+ */
161
+ perHost: Map<PromptInjectionTargetHost, RuntimePromptBudgetObservation>;
162
+ /** Latest per-host observation that is too old to be presented as current. */
163
+ stalePerHost: Map<PromptInjectionTargetHost, RuntimePromptBudgetObservation>;
164
+ }
165
+ export declare function selectConsoleRuntimeBudgetFacts(input: {
166
+ workspaceDir: string;
167
+ targetHost?: PromptInjectionTargetHost;
168
+ now?: Date;
169
+ }): ConsoleRuntimeBudgetFacts;
170
+ /**
171
+ * Rebuild the budget resolution an observation stands for, so console
172
+ * projections run under the SAME resolution shape the real injection used.
173
+ * `fallbackDetail` is deliberately dropped: it is log-only context and never
174
+ * rides events or console payloads.
175
+ */
176
+ export declare function runtimeBudgetObservationToResolution(observation: RuntimePromptBudgetObservation): PromptBudgetResolution;
@@ -0,0 +1,449 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PROMPT_BUDGET_POLICY_VERSION, RUNTIME_V2_PRINCIPLE_BUDGET, isValidContextWindowTokens, promptBudgetCharsForContextWindow, } from '@principles/core/runtime-v2';
4
+ import { isPromptInjectionTargetHost, } from './prompt-injection-projection.js';
5
+ /**
6
+ * PRI-1017: runtime budget OBSERVATION read model over the EXISTING
7
+ * injection-event JSONL (`<workspaceDir>/{.pd,.state}/logs/events_<date>.jsonl`,
8
+ * written by the OpenClaw EventLog and the Codex/WorkBuddy shared path via
9
+ * appendEventLogLine).
10
+ *
11
+ * Why this exists: SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1 forbids the
12
+ * actual injection from running a dynamic budget while the Console forecasts
13
+ * under a different fixed 2000-char budget. The console holds no live host
14
+ * facts, but the RUNTIME persists the resolution it actually used —
15
+ * `runtime_v2_prompt_activations_injected` carries budgetChars, capacity
16
+ * provenance and hostKind. This module reads those facts back so console
17
+ * surfaces forecast under the same budget the last real injection used.
18
+ *
19
+ * Honesty rules this model enforces (SPEC §6.1/§7):
20
+ * - An observation is a FACT ABOUT THE PAST, never a live claim: callers must
21
+ * surface `observedAt`; the console labels it "most recent runtime
22
+ * observation", not a current confirmation.
23
+ * - No logs / no budget facts ⇒ absent, never a fabricated window; the caller
24
+ * keeps the honest fixed-fallback policy.
25
+ * - A latest observation older than RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS is
26
+ * `stale` — the caller must NOT present it as the current budget.
27
+ * - Observations bind to ONE host: OpenClaw's window is never used for a
28
+ * Codex or WorkBuddy forecast.
29
+ * - Untrusted input discipline: every line is parsed as `unknown` and
30
+ * narrowed with typeof guards + closed-set checks; structurally invalid
31
+ * lines are counted in `parseFailures` and skipped loudly, never guessed.
32
+ * - Events predating the model-aware policy carry no provenance fields; they
33
+ * are counted as `legacyEventsSkipped` and produce NO observation — a
34
+ * missing provenance field is "policy unknown", never a capacity claim.
35
+ * - Bounded I/O: only the newest bytes of the most recent daily files are read
36
+ * (tail window), the file cap applying PER candidate log directory, so a
37
+ * runaway or crafted log cannot make a Console request read an unbounded
38
+ * amount of data. Only bounded budget metadata is retained and returned;
39
+ * other event payloads are not cached.
40
+ */
41
+ /**
42
+ * Candidate directories for the runtime-v2 event log, in priority order.
43
+ * Shared with the RuleCode telemetry collector (ERR-031: both readers derive
44
+ * from one list — the v1 EventLog writer emits `.state/logs`, no production
45
+ * code ever created `.pd/logs`).
46
+ */
47
+ export const RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS = ['.pd/logs', '.state/logs'];
48
+ /**
49
+ * Bounded I/O: only the most recent daily files are scanned — PER candidate
50
+ * log directory (`RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS`), not globally. The
51
+ * cap is deliberately per directory: both directories describe the same
52
+ * workspace's log convention and either can hold the newest event for a given
53
+ * host (the reader takes latest-wins ACROSS them), so a global cap would let
54
+ * one directory's files starve the other and drop a live host observation.
55
+ * Worst case per read is therefore `RUNTIME_BUDGET_OBSERVATION_MAX_FILES ×
56
+ * RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS.length` files (14 today) and the same
57
+ * count × `RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES` bytes (56 MiB today).
58
+ */
59
+ export const RUNTIME_BUDGET_OBSERVATION_MAX_FILES = 7;
60
+ /**
61
+ * Bounded I/O: per file, only the newest bytes are read. The writer appends,
62
+ * so the latest observation per host lives at the TAIL; a file larger than
63
+ * this window is tail-read and its older prefix skipped (counted in
64
+ * `truncatedFiles`) instead of materialising an unbounded log in the console
65
+ * process. 4 MiB is ~3.4x the largest real day observed on 2026-10-21
66
+ * (WorkBuddy 1.17 MiB, Codex 0.87 MiB, OpenClaw 0.98 MiB across 7 files).
67
+ */
68
+ export const RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES = 4 * 1024 * 1024;
69
+ /**
70
+ * Maximum age of an observation that may be presented as a CURRENT runtime
71
+ * fact. The runtime refreshes the observation on every prompt build, so an
72
+ * active workspace holds minute-old facts; beyond a day the model may have
73
+ * been switched and the window is no longer evidence about the present.
74
+ */
75
+ export const RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS = 24 * 60 * 60 * 1000;
76
+ /**
77
+ * Clock-skew allowance for event timestamps. A writer clock ahead of ours is
78
+ * normal; an observation further in the future is not a fact about the
79
+ * present and must never be selected as the current budget.
80
+ */
81
+ export const RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
82
+ /** Upper bound on the envelope `ts` string; anything longer is not a timestamp. */
83
+ const OBSERVED_AT_MAX_LENGTH = 64;
84
+ /** Console-side bounded reason: a runtime observation exists but is too old. */
85
+ export const RUNTIME_BUDGET_OBSERVATION_STALE_REASON = 'runtime_budget_observation_stale';
86
+ function isRecord(value) {
87
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
88
+ }
89
+ function nonEmptyString(value) {
90
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
91
+ }
92
+ /** Windows paths are case-insensitive; compare them the way the OS does. */
93
+ function sameWorkspacePath(left, right) {
94
+ const resolvedLeft = path.resolve(left);
95
+ const resolvedRight = path.resolve(right);
96
+ return process.platform === 'win32'
97
+ ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase()
98
+ : resolvedLeft === resolvedRight;
99
+ }
100
+ function parseObservedAt(value) {
101
+ if (value.length > OBSERVED_AT_MAX_LENGTH)
102
+ return null;
103
+ const parsed = Date.parse(value);
104
+ return Number.isFinite(parsed) ? parsed : null;
105
+ }
106
+ const BUDGET_CAPACITY_SOURCES = new Set([
107
+ 'host_effective_window',
108
+ 'pi_catalog_exact',
109
+ 'legacy_fallback',
110
+ ]);
111
+ const BUDGET_CAPACITY_STATUSES = new Set(['confirmed', 'unknown']);
112
+ const BUDGET_FALLBACK_REASONS = new Set([
113
+ 'host_capacity_absent',
114
+ 'host_capacity_source_untrusted',
115
+ 'host_capacity_invalid_value',
116
+ 'catalog_unavailable',
117
+ 'model_identity_absent',
118
+ 'catalog_no_exact_match',
119
+ 'catalog_no_match_for_model_id',
120
+ 'catalog_ambiguous_model_id',
121
+ 'catalog_provider_absent',
122
+ 'catalog_metadata_invalid',
123
+ ]);
124
+ function isBudgetFallbackReason(value) {
125
+ return typeof value === 'string' && BUDGET_FALLBACK_REASONS.has(value);
126
+ }
127
+ /**
128
+ * Narrow one untrusted event line into an observation, or null when it does
129
+ * not carry a trustworthy model-aware budget fact. rc-1/rc-2: every field is
130
+ * guarded; a structurally valid event missing provenance is NOT an
131
+ * observation (see `legacyEventsSkipped` at the report level).
132
+ */
133
+ function narrowObservation(parsed, workspaceDir, nowMs) {
134
+ if (!isRecord(parsed))
135
+ return null;
136
+ if (nonEmptyString(parsed.type) !== 'runtime_v2_prompt_activations_injected')
137
+ return null;
138
+ const observedAt = nonEmptyString(parsed.ts);
139
+ const { data } = parsed;
140
+ if (observedAt === undefined || !isRecord(data))
141
+ return null;
142
+ const observedAtMs = parseObservedAt(observedAt);
143
+ if (observedAtMs === null)
144
+ return null;
145
+ // A timestamp beyond minor clock skew is not a fact about the present; it
146
+ // must not win the "latest observation" race or back a current forecast.
147
+ if (observedAtMs - nowMs > RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS)
148
+ return null;
149
+ // Workspace binding: the console model is workspace-scoped, so an event
150
+ // recorded under a different workspaceDir is not this workspace's fact.
151
+ const eventWorkspaceDir = nonEmptyString(data.workspaceDir);
152
+ if (eventWorkspaceDir === undefined || !sameWorkspacePath(eventWorkspaceDir, workspaceDir))
153
+ return null;
154
+ const rawHostKind = data.hostKind;
155
+ if (!isPromptInjectionTargetHost(rawHostKind))
156
+ return null;
157
+ const { budget } = data;
158
+ if (typeof budget !== 'number' || !Number.isInteger(budget) || budget <= 0)
159
+ return null;
160
+ // A budget computed under another policy version is not evidence about the
161
+ // CURRENT formula. Absent provenance is pre-policy, not "current by
162
+ // default": both are skipped, never reinterpreted.
163
+ if (data.budgetPolicyVersion !== PROMPT_BUDGET_POLICY_VERSION)
164
+ return null;
165
+ const rawSource = data.budgetCapacitySource;
166
+ const rawStatus = data.budgetCapacityStatus;
167
+ if (typeof rawSource !== 'string' || !BUDGET_CAPACITY_SOURCES.has(rawSource))
168
+ return null;
169
+ if (typeof rawStatus !== 'string' || !BUDGET_CAPACITY_STATUSES.has(rawStatus))
170
+ return null;
171
+ const rawWindow = data.budgetContextWindowTokens;
172
+ const rawReason = data.budgetFallbackReason;
173
+ if (rawReason !== undefined && !isBudgetFallbackReason(rawReason))
174
+ return null;
175
+ // Source/status/window/budget must describe ONE coherent resolution: a
176
+ // confirmed source needs a real window and exactly the budget the shared
177
+ // formula derives from it; the legacy fallback needs the legacy budget, no
178
+ // window, and a bounded reason. Anything else is not a trustworthy fact.
179
+ if (rawStatus === 'confirmed') {
180
+ if (rawSource !== 'host_effective_window' && rawSource !== 'pi_catalog_exact')
181
+ return null;
182
+ if (!isValidContextWindowTokens(rawWindow))
183
+ return null;
184
+ if (budget !== promptBudgetCharsForContextWindow(rawWindow))
185
+ return null;
186
+ if (rawReason !== undefined)
187
+ return null;
188
+ }
189
+ else {
190
+ if (rawSource !== 'legacy_fallback')
191
+ return null;
192
+ if (rawWindow !== undefined)
193
+ return null;
194
+ if (!isBudgetFallbackReason(rawReason))
195
+ return null;
196
+ if (budget !== RUNTIME_V2_PRINCIPLE_BUDGET)
197
+ return null;
198
+ }
199
+ return {
200
+ workspaceDir,
201
+ hostKind: rawHostKind,
202
+ budgetChars: budget,
203
+ capacitySource: rawSource,
204
+ capacityStatus: rawStatus,
205
+ contextWindowTokens: rawWindow ?? null,
206
+ fallbackReason: rawReason ?? null,
207
+ policyVersion: PROMPT_BUDGET_POLICY_VERSION,
208
+ observedAt,
209
+ };
210
+ }
211
+ /**
212
+ * Read at most the newest `RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES` of a
213
+ * log file. When the prefix is skipped, the partial first line after the cut
214
+ * is dropped so a half-written JSON line can never be parsed. Returns null
215
+ * when the file cannot be opened/read (the caller skips it loudly elsewhere).
216
+ */
217
+ function readBoundedLogText(filePath) {
218
+ let fd;
219
+ try {
220
+ fd = fs.openSync(filePath, 'r');
221
+ }
222
+ catch {
223
+ return null;
224
+ }
225
+ try {
226
+ const { size } = fs.fstatSync(fd);
227
+ const start = Math.max(0, size - RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES);
228
+ const length = size - start;
229
+ if (length <= 0)
230
+ return { text: '', truncated: false };
231
+ const buffer = Buffer.allocUnsafe(length);
232
+ let offset = 0;
233
+ while (offset < length) {
234
+ const read = fs.readSync(fd, buffer, offset, length - offset, start + offset);
235
+ if (read <= 0)
236
+ break;
237
+ offset += read;
238
+ }
239
+ let text = buffer.subarray(0, offset).toString('utf8');
240
+ if (start > 0) {
241
+ const newline = text.indexOf('\n');
242
+ text = newline === -1 ? '' : text.slice(newline + 1);
243
+ }
244
+ return { text, truncated: start > 0 };
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ finally {
250
+ try {
251
+ fs.closeSync(fd);
252
+ }
253
+ catch { /* fd already closed */ }
254
+ }
255
+ }
256
+ /**
257
+ * Read the most recent budget observation per host from the workspace's
258
+ * runtime-v2 event logs. Never throws: an unreadable log directory degrades
259
+ * to an empty report with an explicit note (absence is UNKNOWN, not zero).
260
+ */
261
+ export function readRuntimePromptBudgetObservations(input) {
262
+ const { workspaceDir } = input;
263
+ const nowMs = (input.now ?? new Date()).getTime();
264
+ const filesRead = [];
265
+ const lines = { count: 0 };
266
+ const parseFailures = { count: 0 };
267
+ const legacyEventsSkipped = { count: 0 };
268
+ const foreignPolicyEventsSkipped = { count: 0 };
269
+ const truncatedFiles = { count: 0 };
270
+ const latest = new Map();
271
+ for (const candidate of RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS) {
272
+ const logsRoot = path.resolve(workspaceDir, ...candidate.split('/'));
273
+ if (!fs.existsSync(logsRoot))
274
+ continue;
275
+ let canonicalRoot;
276
+ try {
277
+ canonicalRoot = fs.realpathSync(logsRoot);
278
+ }
279
+ catch {
280
+ continue;
281
+ }
282
+ let names;
283
+ try {
284
+ names = fs.readdirSync(logsRoot)
285
+ .filter((name) => name.startsWith('events_') && name.endsWith('.jsonl'))
286
+ .sort()
287
+ .slice(-RUNTIME_BUDGET_OBSERVATION_MAX_FILES);
288
+ }
289
+ catch {
290
+ // Unreadable directory contributes nothing; the note stays explicit.
291
+ continue;
292
+ }
293
+ for (const name of names) {
294
+ // Containment guard: only bare event filenames inside the resolved
295
+ // logs root are ever read — a crafted name cannot escape the directory.
296
+ // Canonicalize BOTH sides so an in-logs symlink cannot redirect the
297
+ // read outside the workspace log directory (EP-08).
298
+ const filePath = path.resolve(logsRoot, name);
299
+ if (!filePath.startsWith(logsRoot + path.sep) || path.basename(name) !== name)
300
+ continue;
301
+ let canonicalFile;
302
+ try {
303
+ canonicalFile = fs.realpathSync(filePath);
304
+ }
305
+ catch {
306
+ continue;
307
+ }
308
+ if (!canonicalFile.startsWith(canonicalRoot + path.sep))
309
+ continue;
310
+ filesRead.push(canonicalFile);
311
+ const bounded = readBoundedLogText(canonicalFile);
312
+ if (bounded === null)
313
+ continue;
314
+ if (bounded.truncated)
315
+ truncatedFiles.count += 1;
316
+ for (const line of bounded.text.split('\n')) {
317
+ if (line.trim().length === 0)
318
+ continue;
319
+ lines.count += 1;
320
+ let parsed;
321
+ try {
322
+ parsed = JSON.parse(line);
323
+ }
324
+ catch {
325
+ parseFailures.count += 1;
326
+ continue;
327
+ }
328
+ if (isRecord(parsed) && parsed.type === 'runtime_v2_prompt_activations_injected' && isRecord(parsed.data)) {
329
+ const { data } = parsed;
330
+ const rawPolicyVersion = data.budgetPolicyVersion;
331
+ if (rawPolicyVersion !== undefined && rawPolicyVersion !== PROMPT_BUDGET_POLICY_VERSION) {
332
+ // A budget computed under a DIFFERENT policy version is not
333
+ // evidence about the current formula — counted, not reinterpreted.
334
+ foreignPolicyEventsSkipped.count += 1;
335
+ continue;
336
+ }
337
+ const observation = narrowObservation(parsed, workspaceDir, nowMs);
338
+ if (observation === null) {
339
+ // A pre-policy event, a future/invalid timestamp, incoherent
340
+ // provenance or an unusable binding — counted, never guessed.
341
+ legacyEventsSkipped.count += 1;
342
+ continue;
343
+ }
344
+ const previous = latest.get(observation.hostKind);
345
+ // Compare PARSED instants, not strings: a longer/invalid timestamp
346
+ // must not shadow a real observation lexicographically.
347
+ if (previous === undefined || Date.parse(observation.observedAt) > Date.parse(previous.observedAt)) {
348
+ latest.set(observation.hostKind, observation);
349
+ }
350
+ continue;
351
+ }
352
+ if (isRecord(parsed) && parsed.type === 'runtime_v2_prompt_activations_injected' && !isRecord(parsed.data)) {
353
+ parseFailures.count += 1;
354
+ }
355
+ }
356
+ }
357
+ }
358
+ return {
359
+ observations: [...latest.values()],
360
+ filesRead,
361
+ lines: lines.count,
362
+ parseFailures: parseFailures.count,
363
+ legacyEventsSkipped: legacyEventsSkipped.count,
364
+ foreignPolicyEventsSkipped: foreignPolicyEventsSkipped.count,
365
+ truncatedFiles: truncatedFiles.count,
366
+ note: 'observations are PAST runtime facts bound to one host; absence means UNKNOWN (never a fabricated window); events without model-aware budget provenance, or computed under a different policy version, are skipped, not reinterpreted; the bounded read takes the newest daily files per candidate log directory and files larger than the tail window are counted in truncatedFiles (older prefix skipped, newest event per host still inside the window)',
367
+ };
368
+ }
369
+ /** Latest-wins ordering that treats unparseable timestamps as older, not newer. */
370
+ function compareObservedAt(left, right) {
371
+ const leftMs = Date.parse(left);
372
+ const rightMs = Date.parse(right);
373
+ if (!Number.isFinite(leftMs))
374
+ return Number.isFinite(rightMs) ? -1 : 0;
375
+ if (!Number.isFinite(rightMs))
376
+ return 1;
377
+ return leftMs - rightMs;
378
+ }
379
+ /**
380
+ * Select the observation that may back a console forecast for `targetHost`.
381
+ * `absent` when the host has no observation at all; `stale` when the latest
382
+ * one is older than the freshness bound (the caller must fall back honestly).
383
+ */
384
+ export function selectRuntimeBudgetObservation(input) {
385
+ const now = input.now ?? new Date();
386
+ const candidates = input.targetHost === undefined
387
+ ? [...input.observations]
388
+ : input.observations.filter((observation) => observation.hostKind === input.targetHost);
389
+ let latest;
390
+ for (const candidate of candidates) {
391
+ if (latest === undefined || compareObservedAt(candidate.observedAt, latest.observedAt) > 0)
392
+ latest = candidate;
393
+ }
394
+ if (latest === undefined)
395
+ return { kind: 'absent' };
396
+ const observedAt = Date.parse(latest.observedAt);
397
+ if (!Number.isFinite(observedAt)
398
+ || observedAt - now.getTime() > RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS
399
+ || now.getTime() - observedAt > RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS) {
400
+ return { kind: 'stale', observation: latest };
401
+ }
402
+ return { kind: 'usable', observation: latest };
403
+ }
404
+ export function selectConsoleRuntimeBudgetFacts(input) {
405
+ const report = readRuntimePromptBudgetObservations({ workspaceDir: input.workspaceDir, now: input.now });
406
+ const perHost = new Map();
407
+ const stalePerHost = new Map();
408
+ for (const observation of report.observations) {
409
+ const selection = selectRuntimeBudgetObservation({
410
+ observations: report.observations,
411
+ targetHost: observation.hostKind,
412
+ now: input.now,
413
+ });
414
+ if (selection.kind === 'usable')
415
+ perHost.set(observation.hostKind, selection.observation);
416
+ else if (selection.kind === 'stale')
417
+ stalePerHost.set(observation.hostKind, selection.observation);
418
+ }
419
+ if (input.targetHost === undefined) {
420
+ return { perHost, stalePerHost };
421
+ }
422
+ const selection = selectRuntimeBudgetObservation({
423
+ observations: report.observations,
424
+ targetHost: input.targetHost,
425
+ now: input.now,
426
+ });
427
+ return {
428
+ ...(selection.kind === 'usable' ? { selected: selection.observation } : {}),
429
+ ...(selection.kind === 'stale' ? { stale: selection.observation } : {}),
430
+ perHost,
431
+ stalePerHost,
432
+ };
433
+ }
434
+ /**
435
+ * Rebuild the budget resolution an observation stands for, so console
436
+ * projections run under the SAME resolution shape the real injection used.
437
+ * `fallbackDetail` is deliberately dropped: it is log-only context and never
438
+ * rides events or console payloads.
439
+ */
440
+ export function runtimeBudgetObservationToResolution(observation) {
441
+ return {
442
+ budgetChars: observation.budgetChars,
443
+ capacitySource: observation.capacitySource,
444
+ capacityStatus: observation.capacityStatus,
445
+ contextWindowTokens: observation.contextWindowTokens,
446
+ policyVersion: observation.policyVersion ?? PROMPT_BUDGET_POLICY_VERSION,
447
+ ...(observation.fallbackReason !== null ? { fallbackReason: observation.fallbackReason } : {}),
448
+ };
449
+ }
@@ -232,4 +232,20 @@ export declare function resolveLivePromptInjectionProjection(input: {
232
232
  targetHost?: PromptInjectionTargetHost;
233
233
  excludePrincipleIds?: ReadonlySet<string>;
234
234
  roundKey?: number;
235
+ /**
236
+ * SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1 / PRI-1017: budget resolution
237
+ * the confirmed projection must run under. Console callers pass the
238
+ * resolution rebuilt from the workspace's most recent runtime budget
239
+ * OBSERVATION (`runtimeBudgetObservationToResolution`) so the forecast uses
240
+ * the same budget the last real injection used. Absent → the fixed-fallback
241
+ * default (the honest "no runtime capacity confirmed" policy).
242
+ */
243
+ promptBudget?: PromptBudgetResolution;
244
+ /**
245
+ * PRI-1017: per-host budget resolutions for the unconfirmed per-host
246
+ * forecasts. Each host's projection runs under THAT host's observation —
247
+ * never another host's window. Absent per host → that host's honest
248
+ * fixed-fallback default.
249
+ */
250
+ perHostPromptBudget?: Partial<Record<PromptInjectionTargetHost, PromptBudgetResolution>>;
235
251
  }): Promise<LivePromptInjectionProjectionResolution>;