create-principles-disciple 1.102.0 → 1.104.0

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 (160) hide show
  1. package/console/dist/server/config/pd-config-store.d.ts +50 -1
  2. package/console/dist/server/config/pd-config-store.js +364 -1
  3. package/console/dist/server/models/ActivationsConsoleModel.d.ts +7 -4
  4. package/console/dist/server/models/ActivationsConsoleModel.js +7 -6
  5. package/console/dist/server/routes/config.js +92 -2
  6. package/console/dist/ui/api.d.ts +44 -3
  7. package/console/dist/ui/api.js +58 -2
  8. package/console/dist/ui/components/onboarding/DemoResultView.js +19 -1
  9. package/console/dist/ui/i18n/en.json +63 -2
  10. package/console/dist/ui/i18n/zh-CN.json +63 -2
  11. package/console/dist/ui/pages/activation/ActivationPage.js +11 -11
  12. package/console/dist/ui/pages/activation/ActivationValidators.js +14 -13
  13. package/console/dist/ui/pages/control-center/AgentCard.js +56 -7
  14. package/console/dist/ui/pages/control-center/ControlCenterPage.js +2 -1
  15. package/console/dist/ui/pages/control-center/RuntimeProfileManager.d.ts +31 -0
  16. package/console/dist/ui/pages/control-center/RuntimeProfileManager.js +234 -0
  17. package/console/dist/ui/pages/debt/DebtPage.js +2 -2
  18. package/console/dist/ui/utils/validators.d.ts +26 -3
  19. package/console/dist/ui/utils/validators.js +23 -6
  20. package/console/dist/web/assets/app.css +41 -0
  21. package/console/dist/web/assets/app.js +1918 -1160
  22. package/core/dist/runtime-v2/__tests__/resolve-runtime-config-from-pd-config.test.js +8 -10
  23. package/core/dist/runtime-v2/__tests__/resolve-runtime-config-from-pd-config.test.js.map +1 -1
  24. package/core/dist/runtime-v2/adapter/__tests__/l2-resolve-model-compat.test.d.ts +2 -0
  25. package/core/dist/runtime-v2/adapter/__tests__/l2-resolve-model-compat.test.d.ts.map +1 -0
  26. package/core/dist/runtime-v2/adapter/__tests__/l2-resolve-model-compat.test.js +81 -0
  27. package/core/dist/runtime-v2/adapter/__tests__/l2-resolve-model-compat.test.js.map +1 -0
  28. package/core/dist/runtime-v2/adapter/__tests__/pi-ai-runtime-adapter.test.js +25 -6
  29. package/core/dist/runtime-v2/adapter/__tests__/pi-ai-runtime-adapter.test.js.map +1 -1
  30. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts +8 -0
  31. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
  32. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +4 -1
  33. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
  34. package/core/dist/runtime-v2/candidate-intake-service.d.ts.map +1 -1
  35. package/core/dist/runtime-v2/candidate-intake-service.js +92 -6
  36. package/core/dist/runtime-v2/candidate-intake-service.js.map +1 -1
  37. package/core/dist/runtime-v2/candidate-intake.d.ts +38 -0
  38. package/core/dist/runtime-v2/candidate-intake.d.ts.map +1 -1
  39. package/core/dist/runtime-v2/candidate-intake.js +48 -0
  40. package/core/dist/runtime-v2/candidate-intake.js.map +1 -1
  41. package/core/dist/runtime-v2/config/__tests__/pd-config-agent-binding.test.js +26 -1
  42. package/core/dist/runtime-v2/config/__tests__/pd-config-agent-binding.test.js.map +1 -1
  43. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js +9 -5
  44. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js.map +1 -1
  45. package/core/dist/runtime-v2/config/__tests__/pd-config-validation-edge-cases.test.js +53 -33
  46. package/core/dist/runtime-v2/config/__tests__/pd-config-validation-edge-cases.test.js.map +1 -1
  47. package/core/dist/runtime-v2/config/pd-config-agent-binding.d.ts +2 -0
  48. package/core/dist/runtime-v2/config/pd-config-agent-binding.d.ts.map +1 -1
  49. package/core/dist/runtime-v2/config/pd-config-agent-binding.js +3 -0
  50. package/core/dist/runtime-v2/config/pd-config-agent-binding.js.map +1 -1
  51. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts +7 -3
  52. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
  53. package/core/dist/runtime-v2/config/pd-config-defaults.js +27 -3
  54. package/core/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
  55. package/core/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -1
  56. package/core/dist/runtime-v2/config/pd-config-redaction.js +5 -2
  57. package/core/dist/runtime-v2/config/pd-config-redaction.js.map +1 -1
  58. package/core/dist/runtime-v2/config/pd-config-types.d.ts +6 -0
  59. package/core/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -1
  60. package/core/dist/runtime-v2/config/pd-config-types.js.map +1 -1
  61. package/core/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -1
  62. package/core/dist/runtime-v2/config/pd-config-validate.js +11 -6
  63. package/core/dist/runtime-v2/config/pd-config-validate.js.map +1 -1
  64. package/core/dist/runtime-v2/index.d.ts +2 -1
  65. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  66. package/core/dist/runtime-v2/index.js +1 -0
  67. package/core/dist/runtime-v2/index.js.map +1 -1
  68. package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder-v2.test.js +7 -0
  69. package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder-v2.test.js.map +1 -1
  70. package/core/dist/runtime-v2/internalization/__tests__/diag-rootcause-runner.test.js +7 -6
  71. package/core/dist/runtime-v2/internalization/__tests__/diag-rootcause-runner.test.js.map +1 -1
  72. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +1 -1
  73. package/core/dist/runtime-v2/internalization/diag-distiller-runner.js +4 -4
  74. package/core/dist/runtime-v2/internalization/diag-distiller-runner.js.map +1 -1
  75. package/core/dist/runtime-v2/internalization/diag-rootcause-runner.js +5 -5
  76. package/core/dist/runtime-v2/internalization/diag-rootcause-runner.js.map +1 -1
  77. package/core/dist/runtime-v2/internalization/diag-router-runner.js +4 -4
  78. package/core/dist/runtime-v2/internalization/diag-router-runner.js.map +1 -1
  79. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts +2 -0
  80. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
  81. package/core/dist/runtime-v2/pain-signal-runtime-factory.js +2 -0
  82. package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
  83. package/dist/installer.d.ts.map +1 -1
  84. package/dist/installer.js +5 -1
  85. package/dist/installer.js.map +1 -1
  86. package/dist/mvp-config.d.ts.map +1 -1
  87. package/dist/mvp-config.js +22 -10
  88. package/dist/mvp-config.js.map +1 -1
  89. package/package.json +1 -1
  90. package/pd-cli/dist/commands/__tests__/runtime-probe-config.test.js +2 -2
  91. package/pd-cli/dist/commands/__tests__/runtime-probe-config.test.js.map +1 -1
  92. package/pd-cli/dist/commands/demo-story-a.d.ts.map +1 -1
  93. package/pd-cli/dist/commands/demo-story-a.js +4 -2
  94. package/pd-cli/dist/commands/demo-story-a.js.map +1 -1
  95. package/pd-cli/dist/commands/proven-channel-baseline.d.ts.map +1 -1
  96. package/pd-cli/dist/commands/proven-channel-baseline.js +4 -2
  97. package/pd-cli/dist/commands/proven-channel-baseline.js.map +1 -1
  98. package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
  99. package/pd-cli/dist/commands/runtime-activation.js +8 -2
  100. package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
  101. package/pd-cli/dist/commands/runtime-canary.d.ts.map +1 -1
  102. package/pd-cli/dist/commands/runtime-canary.js +4 -2
  103. package/pd-cli/dist/commands/runtime-canary.js.map +1 -1
  104. package/pd-cli/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
  105. package/pd-cli/dist/commands/runtime-health-snapshot.js +4 -2
  106. package/pd-cli/dist/commands/runtime-health-snapshot.js.map +1 -1
  107. package/pd-cli/dist/commands/runtime-init.js +2 -2
  108. package/pd-cli/dist/commands/runtime-init.js.map +1 -1
  109. package/pd-cli/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
  110. package/pd-cli/dist/commands/runtime-internalization-integrity.js +4 -2
  111. package/pd-cli/dist/commands/runtime-internalization-integrity.js.map +1 -1
  112. package/pd-cli/dist/services/config-doctor.d.ts.map +1 -1
  113. package/pd-cli/dist/services/config-doctor.js +9 -2
  114. package/pd-cli/dist/services/config-doctor.js.map +1 -1
  115. package/pd-cli/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  116. package/pd-cli/dist/services/runtime-adapter-resolver.js +1 -0
  117. package/pd-cli/dist/services/runtime-adapter-resolver.js.map +1 -1
  118. package/plugin/dist/commands/thinking-os.js +25 -7
  119. package/plugin/dist/commands/workflow-debug.d.ts +1 -3
  120. package/plugin/dist/commands/workflow-debug.js +26 -5
  121. package/plugin/dist/core/rule-host.d.ts +5 -2
  122. package/plugin/dist/core/rule-host.js +15 -2
  123. package/plugin/dist/hooks/after-tool-call-helpers.js +20 -3
  124. package/plugin/dist/hooks/gate.js +1 -1
  125. package/plugin/dist/i18n/commands.js +24 -0
  126. package/plugin/dist/index.js +88 -36
  127. package/plugin/dist/service/evolution-dedup.d.ts +1 -1
  128. package/plugin/dist/service/evolution-worker.d.ts +7 -31
  129. package/plugin/dist/service/evolution-worker.js +17 -36
  130. package/plugin/dist/service/queue-io.js +14 -2
  131. package/plugin/dist/service/queue-migration.d.ts +27 -26
  132. package/plugin/dist/service/queue-migration.js +57 -0
  133. package/plugin/docs/COMMAND_REFERENCE.md +0 -1
  134. package/plugin/docs/COMMAND_REFERENCE_EN.md +0 -2
  135. package/plugin/scripts/sync-plugin.mjs +12 -0
  136. package/plugin/templates/langs/en/core/AGENTS.md +2 -2
  137. package/plugin/templates/langs/en/core/TOOLS.md +1 -1
  138. package/plugin/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  139. package/plugin/templates/langs/zh/core/AGENTS.md +2 -2
  140. package/plugin/templates/langs/zh/core/TOOLS.md +1 -1
  141. package/plugin/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  142. package/templates/langs/en/core/AGENTS.md +2 -2
  143. package/templates/langs/en/core/BOOTSTRAP.md +4 -17
  144. package/templates/langs/en/core/TOOLS.md +1 -1
  145. package/templates/langs/en/skills/pd-mentor/SKILL.md +8 -26
  146. package/templates/langs/zh/core/AGENTS.md +2 -2
  147. package/templates/langs/zh/core/BOOTSTRAP.md +4 -17
  148. package/templates/langs/zh/core/TOOLS.md +1 -1
  149. package/templates/langs/zh/skills/pd-mentor/SKILL.md +8 -26
  150. package/plugin/dist/constants/diagnostician.d.ts +0 -12
  151. package/plugin/dist/constants/diagnostician.js +0 -65
  152. package/plugin/dist/service/evolution-queue-migration.d.ts +0 -128
  153. package/plugin/dist/service/evolution-queue-migration.js +0 -53
  154. package/plugin/templates/langs/en/skills/pd-diagnostician/SKILL.md +0 -466
  155. package/plugin/templates/langs/zh/skills/pd-diagnostician/SKILL.md +0 -391
  156. package/templates/langs/en/skills/pd-daily/SKILL.md +0 -199
  157. package/templates/langs/en/skills/pd-grooming/SKILL.md +0 -46
  158. package/templates/langs/zh/skills/pd-daily/SKILL.md +0 -283
  159. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +0 -287
  160. package/templates/langs/zh/skills/pd-grooming/SKILL.md +0 -46
@@ -13,7 +13,7 @@
13
13
  * - ERR-013: Object.hasOwn() for untrusted keys
14
14
  * - ERR-045: ANY-segment redaction for sensitive keys
15
15
  */
16
- import type { EffectivePdConfig, RedactedPdConfigSummary, InternalAgentName, FeatureFlagsResult } from '@principles/core/runtime-v2';
16
+ import type { EffectivePdConfig, RedactedPdConfigSummary, InternalAgentName, RuntimeProfile, FeatureFlagsResult } from '@principles/core/runtime-v2';
17
17
  export interface ConfigLoadResultOk {
18
18
  ok: true;
19
19
  effective: EffectivePdConfig;
@@ -171,4 +171,53 @@ export type FeatureFlagUpdateResult = FeatureFlagUpdateResultOk | FeatureFlagUpd
171
171
  * - ERR-015/018/019: load-validate-merge-write-reload, no stale cache
172
172
  */
173
173
  export declare function updateFeatureFlag(workspaceDir: string, featureName: string, enabled: boolean): FeatureFlagUpdateResult;
174
+ export interface RuntimeProfileCrudResultOk {
175
+ ok: true;
176
+ profileId: string;
177
+ profile: RuntimeProfile;
178
+ }
179
+ export interface RuntimeProfileCrudResultErr {
180
+ ok: false;
181
+ statusCode: number;
182
+ error: string;
183
+ message: string;
184
+ nextAction?: string;
185
+ }
186
+ export type RuntimeProfileCrudResult = RuntimeProfileCrudResultOk | RuntimeProfileCrudResultErr;
187
+ /**
188
+ * Create a new runtime profile in .pd/config.yaml.
189
+ *
190
+ * - Rejects duplicate profile IDs (400 bad_request)
191
+ * - Validates profile structure (type + required fields via validatePdConfig)
192
+ * - Persists atomically, preserving unknown config sections
193
+ *
194
+ * ERR entries:
195
+ * - ERR-001/ERR-005: No `as` bypasses; payload validated as unknown
196
+ * - ERR-002: Graceful degradation includes reason + nextAction
197
+ * - ERR-009/ERR-010: Required fields fail loud
198
+ * - ERR-013: Object.hasOwn() for untrusted keys
199
+ */
200
+ export declare function createRuntimeProfile(workspaceDir: string, profileId: string, payload: unknown): RuntimeProfileCrudResult;
201
+ /**
202
+ * Update an existing runtime profile in .pd/config.yaml.
203
+ *
204
+ * - Rejects non-existent profile (404 not_found)
205
+ * - Rejects type changes (400 bad_request — type change = delete + create)
206
+ * - Merges patch into existing profile, only known fields are persisted
207
+ * - Persists atomically, preserving unknown config sections
208
+ *
209
+ * ERR entries: same as createRuntimeProfile.
210
+ */
211
+ export declare function updateRuntimeProfile(workspaceDir: string, profileId: string, patch: unknown): RuntimeProfileCrudResult;
212
+ /**
213
+ * Delete a runtime profile from .pd/config.yaml.
214
+ *
215
+ * - Rejects non-existent profile (404 not_found)
216
+ * - Rejects if profile is the defaultRuntime (400 bad_request)
217
+ * - Rejects if any agent binding references the profile (400 bad_request, lists agents)
218
+ * - Persists atomically, preserving unknown config sections
219
+ *
220
+ * ERR entries: same as createRuntimeProfile.
221
+ */
222
+ export declare function deleteRuntimeProfile(workspaceDir: string, profileId: string): RuntimeProfileCrudResult;
174
223
  export declare function checkReadiness(workspaceDir: string, agentName: string, getEnvVar?: (name: string) => string | undefined): ReadinessResult | ReadinessError;
@@ -16,7 +16,7 @@
16
16
  import * as fs from 'fs';
17
17
  import * as path from 'path';
18
18
  import yaml from 'js-yaml';
19
- import { validatePdConfig, computeEffectivePdConfig, computeFeatureFlagsFromConfig, redactPdConfig, checkAgentRuntimeReadiness, resolveAgentRuntimeBinding, INTERNAL_AGENT_NAMES, } from '@principles/core/runtime-v2';
19
+ import { validatePdConfig, computeEffectivePdConfig, computeFeatureFlagsFromConfig, redactPdConfig, checkAgentRuntimeReadiness, resolveAgentRuntimeBinding, INTERNAL_AGENT_NAMES, VALID_PROFILE_TYPES, } from '@principles/core/runtime-v2';
20
20
  // ── Constants ────────────────────────────────────────────────────────────────
21
21
  const PD_CONFIG_DIR = '.pd';
22
22
  const PD_CONFIG_FILENAME = 'config.yaml';
@@ -802,6 +802,369 @@ export function updateFeatureFlag(workspaceDir, featureName, enabled) {
802
802
  enabled: confirmedEnabled,
803
803
  };
804
804
  }
805
+ /**
806
+ * Validate the basic structure of a runtime profile payload (type field).
807
+ * Full field validation is delegated to validatePdConfig after merge.
808
+ * Uses Object.hasOwn() for untrusted key checks (ERR-013).
809
+ */
810
+ function validateProfileStructure(payload) {
811
+ if (!isRecord(payload)) {
812
+ return { ok: false, error: 'bad_request', message: 'Profile must be a JSON object with a type field' };
813
+ }
814
+ const typeRaw = Object.hasOwn(payload, 'type') ? payload.type : undefined;
815
+ if (typeRaw === undefined) {
816
+ return { ok: false, error: 'bad_request', message: `Profile missing required field: type (must be one of: ${VALID_PROFILE_TYPES.join(', ')})` };
817
+ }
818
+ if (typeof typeRaw !== 'string' || !VALID_PROFILE_TYPES.includes(typeRaw)) {
819
+ return { ok: false, error: 'bad_request', message: `Profile type must be one of: ${VALID_PROFILE_TYPES.join(', ')}. Got: ${String(typeRaw)}` };
820
+ }
821
+ return { ok: true, type: typeRaw };
822
+ }
823
+ /**
824
+ * Extract only known, correctly-typed fields from a profile payload.
825
+ * Prevents unknown fields from being persisted to .pd/config.yaml.
826
+ * Uses Object.hasOwn() for untrusted key checks (ERR-013).
827
+ */
828
+ function extractProfileFields(payload, type) {
829
+ const result = { type };
830
+ if (Object.hasOwn(payload, 'provider') && typeof payload.provider === 'string') {
831
+ result.provider = payload.provider;
832
+ }
833
+ if (Object.hasOwn(payload, 'model') && typeof payload.model === 'string') {
834
+ result.model = payload.model;
835
+ }
836
+ if (type === 'openclaw') {
837
+ if (Object.hasOwn(payload, 'source') && typeof payload.source === 'string') {
838
+ result.source = payload.source;
839
+ }
840
+ }
841
+ else {
842
+ // pi-ai
843
+ if (Object.hasOwn(payload, 'apiKeyEnv') && typeof payload.apiKeyEnv === 'string') {
844
+ result.apiKeyEnv = payload.apiKeyEnv;
845
+ }
846
+ if (Object.hasOwn(payload, 'baseUrl') && typeof payload.baseUrl === 'string') {
847
+ result.baseUrl = payload.baseUrl;
848
+ }
849
+ if (Object.hasOwn(payload, 'timeoutMs') && typeof payload.timeoutMs === 'number') {
850
+ result.timeoutMs = payload.timeoutMs;
851
+ }
852
+ if (Object.hasOwn(payload, 'maxRetries') && typeof payload.maxRetries === 'number') {
853
+ result.maxRetries = payload.maxRetries;
854
+ }
855
+ }
856
+ return result;
857
+ }
858
+ /**
859
+ * Read raw config file preserving unknown root sections.
860
+ * Returns an empty record if the file does not exist.
861
+ * Treats parsed YAML as unknown (ERR-001).
862
+ */
863
+ function readRawConfig(configPath) {
864
+ if (!fs.existsSync(configPath)) {
865
+ return {};
866
+ }
867
+ const rawContent = fs.readFileSync(configPath, 'utf8');
868
+ const rawParsed = yaml.load(rawContent, { schema: yaml.JSON_SCHEMA });
869
+ if (isRecord(rawParsed)) {
870
+ return { ...rawParsed };
871
+ }
872
+ return {};
873
+ }
874
+ /**
875
+ * Create a new runtime profile in .pd/config.yaml.
876
+ *
877
+ * - Rejects duplicate profile IDs (400 bad_request)
878
+ * - Validates profile structure (type + required fields via validatePdConfig)
879
+ * - Persists atomically, preserving unknown config sections
880
+ *
881
+ * ERR entries:
882
+ * - ERR-001/ERR-005: No `as` bypasses; payload validated as unknown
883
+ * - ERR-002: Graceful degradation includes reason + nextAction
884
+ * - ERR-009/ERR-010: Required fields fail loud
885
+ * - ERR-013: Object.hasOwn() for untrusted keys
886
+ */
887
+ export function createRuntimeProfile(workspaceDir, profileId, payload) {
888
+ // 1. Validate profileId
889
+ if (typeof profileId !== 'string' || profileId.length === 0) {
890
+ return { ok: false, statusCode: 400, error: 'bad_request', message: 'profileId must be a non-empty string' };
891
+ }
892
+ // 2. Validate profile structure (type field)
893
+ const structResult = validateProfileStructure(payload);
894
+ if (!structResult.ok) {
895
+ return { ok: false, statusCode: 400, error: structResult.error, message: structResult.message };
896
+ }
897
+ // 3. Load existing config — refuse to write on malformed (rc-9)
898
+ const loadResult = loadPdConfig(workspaceDir);
899
+ if (!loadResult.ok) {
900
+ return {
901
+ ok: false,
902
+ statusCode: 409,
903
+ error: 'conflict',
904
+ message: `Cannot create runtime profile: existing .pd/config.yaml is malformed. Fix config errors first. Errors: ${loadResult.errors.map(e => e.reason).join('; ')}`,
905
+ };
906
+ }
907
+ const { effective } = loadResult;
908
+ // 4. Reject duplicate profile ID
909
+ if (Object.hasOwn(effective.config.runtimeProfiles, profileId)) {
910
+ return {
911
+ ok: false,
912
+ statusCode: 400,
913
+ error: 'bad_request',
914
+ message: `Runtime profile '${profileId}' already exists. Use PATCH to update it.`,
915
+ nextAction: `Use PATCH /api/v1/config/profiles/${encodeURIComponent(profileId)} to update the existing profile`,
916
+ };
917
+ }
918
+ // 5. Extract only known fields from payload (prevents unknown field persistence)
919
+ const profileRecord = extractProfileFields(payload, structResult.type);
920
+ // 6. Read raw config to preserve unknown sections, add new profile
921
+ const configPath = getPdConfigPath(workspaceDir);
922
+ const rawConfig = readRawConfig(configPath);
923
+ const profilesMap = isRecord(rawConfig.runtimeProfiles)
924
+ ? { ...rawConfig.runtimeProfiles }
925
+ : { ...effective.config.runtimeProfiles };
926
+ profilesMap[profileId] = profileRecord;
927
+ rawConfig.runtimeProfiles = profilesMap;
928
+ // 7. Validate the full updated config (catches missing required fields etc.)
929
+ const validation = validatePdConfig(rawConfig);
930
+ if (!validation.ok) {
931
+ return {
932
+ ok: false,
933
+ statusCode: 400,
934
+ error: 'bad_request',
935
+ message: `Profile validation failed: ${validation.errors.map(e => e.reason).join('; ')}`,
936
+ nextAction: 'Fix profile fields and retry',
937
+ };
938
+ }
939
+ // 8. Atomic write
940
+ try {
941
+ writeConfigAtomic(configPath, rawConfig);
942
+ }
943
+ catch (err) {
944
+ const message = err instanceof Error ? err.message : String(err);
945
+ return { ok: false, statusCode: 500, error: 'write_error', message: `Failed to write config: ${message}`, nextAction: 'Check disk space and file permissions for .pd/config.yaml' };
946
+ }
947
+ // 9. Return the validated profile from the config
948
+ // Defensive guard: Object.hasOwn() doesn't narrow TS type, so extract and verify.
949
+ const createdProfile = validation.value.runtimeProfiles[profileId];
950
+ if (!createdProfile) {
951
+ return { ok: false, statusCode: 500, error: 'internal_error',
952
+ message: `Profile '${profileId}' was written but could not be read back from config.`,
953
+ nextAction: 'This is a bug in PD config persistence. Please report it.' };
954
+ }
955
+ return {
956
+ ok: true,
957
+ profileId,
958
+ profile: createdProfile,
959
+ };
960
+ }
961
+ /**
962
+ * Update an existing runtime profile in .pd/config.yaml.
963
+ *
964
+ * - Rejects non-existent profile (404 not_found)
965
+ * - Rejects type changes (400 bad_request — type change = delete + create)
966
+ * - Merges patch into existing profile, only known fields are persisted
967
+ * - Persists atomically, preserving unknown config sections
968
+ *
969
+ * ERR entries: same as createRuntimeProfile.
970
+ */
971
+ export function updateRuntimeProfile(workspaceDir, profileId, patch) {
972
+ // 1. Validate profileId
973
+ if (typeof profileId !== 'string' || profileId.length === 0) {
974
+ return { ok: false, statusCode: 400, error: 'bad_request', message: 'profileId must be a non-empty string' };
975
+ }
976
+ // 2. Validate patch is a record
977
+ if (!isRecord(patch)) {
978
+ return { ok: false, statusCode: 400, error: 'bad_request', message: 'Patch payload must be a JSON object' };
979
+ }
980
+ // 3. Load existing config — refuse to write on malformed (rc-9)
981
+ const loadResult = loadPdConfig(workspaceDir);
982
+ if (!loadResult.ok) {
983
+ return {
984
+ ok: false,
985
+ statusCode: 409,
986
+ error: 'conflict',
987
+ message: `Cannot update runtime profile: existing .pd/config.yaml is malformed. Fix config errors first. Errors: ${loadResult.errors.map(e => e.reason).join('; ')}`,
988
+ };
989
+ }
990
+ const { effective } = loadResult;
991
+ // 4. Reject if profile doesn't exist (404)
992
+ if (!Object.hasOwn(effective.config.runtimeProfiles, profileId)) {
993
+ return {
994
+ ok: false,
995
+ statusCode: 404,
996
+ error: 'not_found',
997
+ message: `Runtime profile '${profileId}' does not exist. Available profiles: ${Object.keys(effective.config.runtimeProfiles).join(', ')}`,
998
+ nextAction: 'Use POST /api/v1/config/profiles to create a new profile',
999
+ };
1000
+ }
1001
+ const existingProfile = effective.config.runtimeProfiles[profileId];
1002
+ // Defensive guard: Object.hasOwn() above doesn't narrow TS type.
1003
+ if (!existingProfile) {
1004
+ return { ok: false, statusCode: 500, error: 'internal_error',
1005
+ message: `Profile '${profileId}' existence check passed but value is undefined.`,
1006
+ nextAction: 'This is a bug in PD config resolution. Please report it.' };
1007
+ }
1008
+ // 5. Reject type change (type change = delete + create)
1009
+ if (Object.hasOwn(patch, 'type')) {
1010
+ const patchType = patch.type;
1011
+ if (typeof patchType !== 'string' || patchType !== existingProfile.type) {
1012
+ return {
1013
+ ok: false,
1014
+ statusCode: 400,
1015
+ error: 'bad_request',
1016
+ message: `Cannot change profile type (existing: '${existingProfile.type}', requested: '${String(patchType)}'). Type change equals delete + create. Delete this profile and create a new one instead.`,
1017
+ nextAction: `DELETE /api/v1/config/profiles/${encodeURIComponent(profileId)} then POST /api/v1/config/profiles with the new type`,
1018
+ };
1019
+ }
1020
+ }
1021
+ // 6. Merge patch into existing profile, then extract only known fields
1022
+ const mergedSource = { ...existingProfile, ...patch };
1023
+ const mergedRecord = extractProfileFields(mergedSource, existingProfile.type);
1024
+ // 7. Read raw config to preserve unknown sections, update the profile
1025
+ const configPath = getPdConfigPath(workspaceDir);
1026
+ const rawConfig = readRawConfig(configPath);
1027
+ const profilesMap = isRecord(rawConfig.runtimeProfiles)
1028
+ ? { ...rawConfig.runtimeProfiles }
1029
+ : { ...effective.config.runtimeProfiles };
1030
+ profilesMap[profileId] = mergedRecord;
1031
+ rawConfig.runtimeProfiles = profilesMap;
1032
+ // 8. Validate the full updated config
1033
+ const validation = validatePdConfig(rawConfig);
1034
+ if (!validation.ok) {
1035
+ return {
1036
+ ok: false,
1037
+ statusCode: 400,
1038
+ error: 'bad_request',
1039
+ message: `Profile validation failed: ${validation.errors.map(e => e.reason).join('; ')}`,
1040
+ nextAction: 'Fix profile fields and retry',
1041
+ };
1042
+ }
1043
+ // 9. Atomic write
1044
+ try {
1045
+ writeConfigAtomic(configPath, rawConfig);
1046
+ }
1047
+ catch (err) {
1048
+ const message = err instanceof Error ? err.message : String(err);
1049
+ return { ok: false, statusCode: 500, error: 'write_error', message: `Failed to write config: ${message}`, nextAction: 'Check disk space and file permissions for .pd/config.yaml' };
1050
+ }
1051
+ // 10. Return the validated updated profile
1052
+ // Defensive guard: Object.hasOwn() doesn't narrow TS type.
1053
+ const updatedProfile = validation.value.runtimeProfiles[profileId];
1054
+ if (!updatedProfile) {
1055
+ return { ok: false, statusCode: 500, error: 'internal_error',
1056
+ message: `Profile '${profileId}' was updated but could not be read back from config.`,
1057
+ nextAction: 'This is a bug in PD config persistence. Please report it.' };
1058
+ }
1059
+ return {
1060
+ ok: true,
1061
+ profileId,
1062
+ profile: updatedProfile,
1063
+ };
1064
+ }
1065
+ /**
1066
+ * Delete a runtime profile from .pd/config.yaml.
1067
+ *
1068
+ * - Rejects non-existent profile (404 not_found)
1069
+ * - Rejects if profile is the defaultRuntime (400 bad_request)
1070
+ * - Rejects if any agent binding references the profile (400 bad_request, lists agents)
1071
+ * - Persists atomically, preserving unknown config sections
1072
+ *
1073
+ * ERR entries: same as createRuntimeProfile.
1074
+ */
1075
+ export function deleteRuntimeProfile(workspaceDir, profileId) {
1076
+ // 1. Validate profileId
1077
+ if (typeof profileId !== 'string' || profileId.length === 0) {
1078
+ return { ok: false, statusCode: 400, error: 'bad_request', message: 'profileId must be a non-empty string' };
1079
+ }
1080
+ // 2. Load existing config — refuse to write on malformed (rc-9)
1081
+ const loadResult = loadPdConfig(workspaceDir);
1082
+ if (!loadResult.ok) {
1083
+ return {
1084
+ ok: false,
1085
+ statusCode: 409,
1086
+ error: 'conflict',
1087
+ message: `Cannot delete runtime profile: existing .pd/config.yaml is malformed. Fix config errors first. Errors: ${loadResult.errors.map(e => e.reason).join('; ')}`,
1088
+ };
1089
+ }
1090
+ const { effective } = loadResult;
1091
+ // 3. Reject if profile doesn't exist (404)
1092
+ if (!Object.hasOwn(effective.config.runtimeProfiles, profileId)) {
1093
+ return {
1094
+ ok: false,
1095
+ statusCode: 404,
1096
+ error: 'not_found',
1097
+ message: `Runtime profile '${profileId}' does not exist. Available profiles: ${Object.keys(effective.config.runtimeProfiles).join(', ')}`,
1098
+ };
1099
+ }
1100
+ // Capture the profile snapshot BEFORE deletion (for return value).
1101
+ // Defensive guard: Object.hasOwn() doesn't narrow TS type.
1102
+ const deletedProfile = effective.config.runtimeProfiles[profileId];
1103
+ if (!deletedProfile) {
1104
+ return { ok: false, statusCode: 500, error: 'internal_error',
1105
+ message: `Profile '${profileId}' existence check passed but value is undefined.`,
1106
+ nextAction: 'This is a bug in PD config resolution. Please report it.' };
1107
+ }
1108
+ // 4. Reject if profile is the defaultRuntime
1109
+ if (effective.config.internalAgents.defaultRuntime === profileId) {
1110
+ return {
1111
+ ok: false,
1112
+ statusCode: 400,
1113
+ error: 'bad_request',
1114
+ message: `Cannot delete runtime profile '${profileId}' because it is the default runtime. Change the default runtime first.`,
1115
+ nextAction: 'Use PATCH /api/v1/config/default-runtime to change the default to another profile before deleting',
1116
+ };
1117
+ }
1118
+ // 5. Reject if any agent binding references this profile (list the agents)
1119
+ const referencingAgents = [];
1120
+ for (const agentName of INTERNAL_AGENT_NAMES) {
1121
+ const binding = effective.config.internalAgents.agents[agentName];
1122
+ if (binding && binding.runtimeProfile === profileId) {
1123
+ referencingAgents.push(agentName);
1124
+ }
1125
+ }
1126
+ if (referencingAgents.length > 0) {
1127
+ return {
1128
+ ok: false,
1129
+ statusCode: 400,
1130
+ error: 'bad_request',
1131
+ message: `Cannot delete runtime profile '${profileId}' because it is referenced by agents: ${referencingAgents.join(', ')}. Update their bindings first.`,
1132
+ nextAction: `Use PATCH /api/v1/config/agents/:agentName/binding to reassign: ${referencingAgents.join(', ')}`,
1133
+ };
1134
+ }
1135
+ // 6. Read raw config to preserve unknown sections, remove the profile
1136
+ const configPath = getPdConfigPath(workspaceDir);
1137
+ const rawConfig = readRawConfig(configPath);
1138
+ const profilesMap = isRecord(rawConfig.runtimeProfiles)
1139
+ ? { ...rawConfig.runtimeProfiles }
1140
+ : {};
1141
+ delete profilesMap[profileId];
1142
+ rawConfig.runtimeProfiles = profilesMap;
1143
+ // 7. Validate the updated config
1144
+ const validation = validatePdConfig(rawConfig);
1145
+ if (!validation.ok) {
1146
+ return {
1147
+ ok: false,
1148
+ statusCode: 400,
1149
+ error: 'bad_request',
1150
+ message: `Updated config would be invalid after deletion: ${validation.errors.map(e => e.reason).join('; ')}`,
1151
+ };
1152
+ }
1153
+ // 8. Atomic write
1154
+ try {
1155
+ writeConfigAtomic(configPath, rawConfig);
1156
+ }
1157
+ catch (err) {
1158
+ const message = err instanceof Error ? err.message : String(err);
1159
+ return { ok: false, statusCode: 500, error: 'write_error', message: `Failed to write config: ${message}`, nextAction: 'Check disk space and file permissions for .pd/config.yaml' };
1160
+ }
1161
+ // 9. Return the deleted profile snapshot for client confirmation
1162
+ return {
1163
+ ok: true,
1164
+ profileId,
1165
+ profile: deletedProfile,
1166
+ };
1167
+ }
805
1168
  // ── Check Readiness ──────────────────────────────────────────────────────────
806
1169
  export function checkReadiness(workspaceDir, agentName, getEnvVar = (name) => process.env[name]) {
807
1170
  // 1. Validate agent name
@@ -8,7 +8,7 @@
8
8
  * even when the rulecode_context_v2 flag is off.
9
9
  */
10
10
  export interface ActivationRecord {
11
- id: string;
11
+ activationId: string;
12
12
  artifactId: string;
13
13
  principleId: string;
14
14
  channel: string;
@@ -46,9 +46,12 @@ export interface ActivationRecord {
46
46
  }
47
47
  export interface ActivationsResponse {
48
48
  activations: ActivationRecord[];
49
- generatedAt: string;
50
- /** Present when data is degraded/missing rather than genuinely empty */
51
- note?: string;
49
+ /** 'ok' on success, 'degraded' when data is incomplete/missing. */
50
+ status: 'ok' | 'degraded';
51
+ /** Present when status is 'degraded' — explains why. */
52
+ reason?: string;
53
+ /** Present when status is 'degraded' — next operator action. */
54
+ nextAction?: string;
52
55
  }
53
56
  export declare class ActivationsConsoleModel {
54
57
  private readonly workspaceDir;
@@ -42,7 +42,7 @@ export class ActivationsConsoleModel {
42
42
  async getActivations() {
43
43
  const stateDbPath = path.join(this.workspaceDir, '.pd', 'state.db');
44
44
  if (!fs.existsSync(stateDbPath)) {
45
- return { activations: [], generatedAt: new Date().toISOString(), note: 'state.db not found — workspace may not be initialized' };
45
+ return { activations: [], status: 'degraded', reason: 'state.db not found — workspace may not be initialized', nextAction: 'Run pd runtime diagnostics to check workspace state' };
46
46
  }
47
47
  const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
48
48
  try {
@@ -54,7 +54,7 @@ export class ActivationsConsoleModel {
54
54
  }
55
55
  catch (err) {
56
56
  if (isMissingTableError(err)) {
57
- return { activations: [], generatedAt: new Date().toISOString(), note: 'activation table not found — workspace may not be initialized' };
57
+ return { activations: [], status: 'degraded', reason: 'activation table not found — workspace may not be initialized', nextAction: 'Run pd runtime internalization integrity to check schema' };
58
58
  }
59
59
  throw err;
60
60
  }
@@ -156,7 +156,7 @@ export class ActivationsConsoleModel {
156
156
  }
157
157
  }
158
158
  const enriched = {
159
- id: record.activationId,
159
+ activationId: record.activationId,
160
160
  artifactId: record.artifactId,
161
161
  principleId: artifactPrincipleMap.get(record.artifactId) ?? 'unlinked',
162
162
  channel: record.channel,
@@ -181,13 +181,14 @@ export class ActivationsConsoleModel {
181
181
  if (danglingArtifactIds.size > 0) {
182
182
  return {
183
183
  activations: facts,
184
- generatedAt: new Date().toISOString(),
185
- note: `${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s): ${Array.from(danglingArtifactIds).join(', ')}`,
184
+ status: 'degraded',
185
+ reason: `${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s): ${Array.from(danglingArtifactIds).join(', ')}`,
186
+ nextAction: 'Run pd runtime internalization integrity to check for orphaned activations',
186
187
  };
187
188
  }
188
189
  return {
189
190
  activations: facts,
190
- generatedAt: new Date().toISOString(),
191
+ status: 'ok',
191
192
  };
192
193
  }
193
194
  finally {
@@ -20,8 +20,8 @@
20
20
  * - ERR-014/ERR-016/ERR-017: Safe serialization for previews
21
21
  * - ERR-045: ANY-segment redaction for sensitive keys
22
22
  */
23
- import { sendSuccess, sendError, sendNotFound, sendMethodNotAllowed, sendBadRequest } from '../utils/response.js';
24
- import { getConfigSummary, getConfigCatalog, updateAgentBinding, updateDefaultRuntime, checkReadiness, getPrinciplesOutputLanguage, updatePrinciplesOutputLanguage, updateFeatureFlag, } from '../config/pd-config-store.js';
23
+ import { sendSuccess, sendError, sendNotFound, sendMethodNotAllowed, sendBadRequest, sendJson } from '../utils/response.js';
24
+ import { getConfigSummary, getConfigCatalog, updateAgentBinding, updateDefaultRuntime, checkReadiness, getPrinciplesOutputLanguage, updatePrinciplesOutputLanguage, updateFeatureFlag, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, } from '../config/pd-config-store.js';
25
25
  // ── Constants ────────────────────────────────────────────────────────────────
26
26
  const MAX_BODY_SIZE = 1024 * 64; // 64 KB — generous for config binding updates
27
27
  // ── Request Body Reader ──────────────────────────────────────────────────────
@@ -262,6 +262,96 @@ export async function handleConfigRoute(req, res, ctx) {
262
262
  sendMethodNotAllowed(res);
263
263
  return;
264
264
  }
265
+ // POST /profiles — create a new runtime profile
266
+ if (subPath === '/profiles') {
267
+ if (method !== 'POST') {
268
+ sendMethodNotAllowed(res);
269
+ return;
270
+ }
271
+ let bodyText;
272
+ try {
273
+ bodyText = await readBody(req);
274
+ }
275
+ catch {
276
+ sendBadRequest(res, 'Request body exceeds maximum allowed size');
277
+ return;
278
+ }
279
+ const body = safeParseBody(bodyText);
280
+ if (body === null) {
281
+ sendBadRequest(res, 'Invalid JSON body');
282
+ return;
283
+ }
284
+ // Validate body shape: { id: string, profile: object } (ERR-001: no `as` bypass)
285
+ if (!isObject(body)) {
286
+ sendBadRequest(res, 'Body must be a JSON object with id and profile fields');
287
+ return;
288
+ }
289
+ if (!Object.hasOwn(body, 'id')) {
290
+ sendBadRequest(res, 'Missing required field: id');
291
+ return;
292
+ }
293
+ if (typeof body.id !== 'string' || body.id.length === 0) {
294
+ sendBadRequest(res, 'id must be a non-empty string');
295
+ return;
296
+ }
297
+ if (!Object.hasOwn(body, 'profile')) {
298
+ sendBadRequest(res, 'Missing required field: profile');
299
+ return;
300
+ }
301
+ const result = createRuntimeProfile(workspaceDir, body.id, body.profile);
302
+ if (!result.ok) {
303
+ sendError(res, result.statusCode, result.error, result.message);
304
+ return;
305
+ }
306
+ // 201 Created for new resource
307
+ sendJson(res, 201, { success: true, data: { profileId: result.profileId, profile: result.profile } });
308
+ return;
309
+ }
310
+ // PATCH/DELETE /profiles/:profileId — update or delete a runtime profile
311
+ const profileMatch = /^\/profiles\/([^/]+)$/.exec(subPath);
312
+ if (profileMatch) {
313
+ let profileId;
314
+ try {
315
+ profileId = decodeURIComponent(profileMatch[1] ?? '');
316
+ }
317
+ catch {
318
+ sendBadRequest(res, 'Invalid profile ID encoding');
319
+ return;
320
+ }
321
+ if (method === 'PATCH') {
322
+ let bodyText;
323
+ try {
324
+ bodyText = await readBody(req);
325
+ }
326
+ catch {
327
+ sendBadRequest(res, 'Request body exceeds maximum allowed size');
328
+ return;
329
+ }
330
+ const body = safeParseBody(bodyText);
331
+ if (body === null) {
332
+ sendBadRequest(res, 'Invalid JSON body');
333
+ return;
334
+ }
335
+ const result = updateRuntimeProfile(workspaceDir, profileId, body);
336
+ if (!result.ok) {
337
+ sendError(res, result.statusCode, result.error, result.message);
338
+ return;
339
+ }
340
+ sendSuccess(res, { profileId: result.profileId, profile: result.profile });
341
+ return;
342
+ }
343
+ if (method === 'DELETE') {
344
+ const result = deleteRuntimeProfile(workspaceDir, profileId);
345
+ if (!result.ok) {
346
+ sendError(res, result.statusCode, result.error, result.message);
347
+ return;
348
+ }
349
+ sendSuccess(res, { profileId: result.profileId, profile: result.profile });
350
+ return;
351
+ }
352
+ sendMethodNotAllowed(res);
353
+ return;
354
+ }
265
355
  // 404 fallback
266
356
  sendNotFound(res, `Route /api/v1/config${subPath} not found`);
267
357
  }