principles-disciple 1.72.0 → 1.74.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 (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +29 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. package/ui/src/utils/format.ts +0 -15
@@ -0,0 +1,154 @@
1
+ import { SystemLogger } from './system-logger.js';
2
+
3
+ const PAIN_DIAGNOSTIC_SOURCES = [
4
+ 'manual',
5
+ 'tool_failure',
6
+ 'dispatch_error',
7
+ 'gate_blocked',
8
+ 'user_empathy',
9
+ 'llm_paralysis',
10
+ 'semantic',
11
+ 'subagent_error',
12
+ ] as const;
13
+
14
+ export type PainDiagnosticSource = typeof PAIN_DIAGNOSTIC_SOURCES[number];
15
+
16
+ export type PainDiagnosticGateReason =
17
+ | 'manual'
18
+ | 'high_gfi'
19
+ | 'repeated_failure'
20
+ | 'semantic_pain'
21
+ | 'llm_paralysis'
22
+ | 'risky_high_score'
23
+ | 'subagent_error'
24
+ | 'gate_blocked'
25
+ | 'cooldown'
26
+ | 'below_gate';
27
+
28
+ export interface PainDiagnosticGateInput {
29
+ source: PainDiagnosticSource | string;
30
+ score: number;
31
+ currentGfi: number;
32
+ consecutiveErrors?: number;
33
+ isRisky?: boolean;
34
+ errorHash?: string;
35
+ sessionId?: string;
36
+ nowMs?: number;
37
+ cooldownMs?: number;
38
+ thresholds?: {
39
+ painTrigger?: number;
40
+ highSeverity?: number;
41
+ highGfi?: number;
42
+ repeatedFailure?: number;
43
+ semanticPain?: number;
44
+ };
45
+ }
46
+
47
+ export interface PainDiagnosticGateDecision {
48
+ shouldDiagnose: boolean;
49
+ reason: PainDiagnosticGateReason;
50
+ episodeKey: string;
51
+ detail: string;
52
+ }
53
+
54
+ const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000;
55
+ const lastDiagnosedAtByEpisode = new Map<string, number>();
56
+
57
+ function normalizedSource(source: string): PainDiagnosticSource | string {
58
+ if (source.startsWith('llm_') && source !== 'llm_paralysis') {
59
+ return 'semantic';
60
+ }
61
+ if (!(PAIN_DIAGNOSTIC_SOURCES as readonly string[]).includes(source)) {
62
+ SystemLogger.log('', 'GATE_UNKNOWN_SOURCE', `Unknown pain source: "${source}"`);
63
+ }
64
+ return source;
65
+ }
66
+
67
+ function buildEpisodeKey(input: PainDiagnosticGateInput): string {
68
+ const source = normalizedSource(input.source);
69
+ const sessionId = input.sessionId || 'unknown';
70
+ const hash = input.errorHash || 'no-hash';
71
+ return `${sessionId}:${source}:${hash}`;
72
+ }
73
+
74
+ function withinCooldown(input: PainDiagnosticGateInput, episodeKey: string): boolean {
75
+ const cooldownMs = input.cooldownMs ?? DEFAULT_COOLDOWN_MS;
76
+ if (cooldownMs <= 0) return false;
77
+
78
+ const nowMs = input.nowMs ?? Date.now();
79
+ const last = lastDiagnosedAtByEpisode.get(episodeKey);
80
+ return last !== undefined && nowMs - last < cooldownMs;
81
+ }
82
+
83
+ function markDiagnosed(input: PainDiagnosticGateInput, episodeKey: string): void {
84
+ lastDiagnosedAtByEpisode.set(episodeKey, input.nowMs ?? Date.now());
85
+ }
86
+
87
+ export function resetPainDiagnosticGateForTest(): void {
88
+ lastDiagnosedAtByEpisode.clear();
89
+ }
90
+
91
+ export function evaluatePainDiagnosticGate(input: PainDiagnosticGateInput): PainDiagnosticGateDecision {
92
+ const source = normalizedSource(input.source);
93
+ const episodeKey = buildEpisodeKey(input);
94
+ const painTrigger = input.thresholds?.painTrigger ?? 40;
95
+ const highSeverity = input.thresholds?.highSeverity ?? 70;
96
+ const highGfi = input.thresholds?.highGfi ?? Math.max(highSeverity, painTrigger + 30);
97
+ const repeatedFailure = input.thresholds?.repeatedFailure ?? 4;
98
+ const semanticPain = input.thresholds?.semanticPain ?? Math.max(painTrigger, 60);
99
+ const score = Number.isFinite(input.score) ? input.score : 0;
100
+ const currentGfi = Number.isFinite(input.currentGfi) ? input.currentGfi : 0;
101
+ const consecutiveErrors: number = Number.isFinite(input.consecutiveErrors) ? (input.consecutiveErrors as number) : 0;
102
+
103
+ const approve = (reason: PainDiagnosticGateReason, detail: string): PainDiagnosticGateDecision => {
104
+ if (withinCooldown(input, episodeKey)) {
105
+ return {
106
+ shouldDiagnose: false,
107
+ reason: 'cooldown',
108
+ episodeKey,
109
+ detail: `recently diagnosed; ${detail}`,
110
+ };
111
+ }
112
+ markDiagnosed(input, episodeKey);
113
+ return { shouldDiagnose: true, reason, episodeKey, detail };
114
+ };
115
+
116
+ if (source === 'manual') {
117
+ return approve('manual', 'manual pain signal bypasses automatic gate');
118
+ }
119
+
120
+ if (source === 'subagent_error' && score >= painTrigger) {
121
+ return approve('subagent_error', `subagent error score ${score} >= ${painTrigger}`);
122
+ }
123
+
124
+ if (source === 'llm_paralysis' && score >= painTrigger) {
125
+ return approve('llm_paralysis', `llm paralysis score ${score} >= ${painTrigger}`);
126
+ }
127
+
128
+ if (source === 'gate_blocked' && score >= painTrigger) {
129
+ return approve('gate_blocked', `gate blocked score ${score} >= ${painTrigger}`);
130
+ }
131
+
132
+ if ((source === 'user_empathy' || source === 'semantic') && score >= semanticPain) {
133
+ return approve('semantic_pain', `semantic pain score ${score} >= ${semanticPain}`);
134
+ }
135
+
136
+ if (input.isRisky === true && score >= highSeverity) {
137
+ return approve('risky_high_score', `risky operation score ${score} >= ${highSeverity}`);
138
+ }
139
+
140
+ if (consecutiveErrors >= repeatedFailure) {
141
+ return approve('repeated_failure', `consecutive errors ${consecutiveErrors} >= ${repeatedFailure}`);
142
+ }
143
+
144
+ if (currentGfi >= highGfi) {
145
+ return approve('high_gfi', `GFI ${currentGfi.toFixed(1)} >= ${highGfi}`);
146
+ }
147
+
148
+ return {
149
+ shouldDiagnose: false,
150
+ reason: 'below_gate',
151
+ episodeKey,
152
+ detail: `score=${score}; gfi=${currentGfi.toFixed(1)}; consecutive=${consecutiveErrors}`,
153
+ };
154
+ }
@@ -1,139 +1,22 @@
1
- /**
2
- * Universal PainSignal schema for the Evolution SDK.
3
- *
4
- * This module defines a framework-agnostic pain signal that any AI agent
5
- * framework can produce. It extends the existing PainFlagData format with
6
- * additional structured fields (domain, severity, context) needed for
7
- * cross-workspace evolution and multi-domain support.
8
- *
9
- * Validation uses @sinclair/typebox to match existing project patterns.
10
- */
11
- import { Type, type Static } from '@sinclair/typebox';
12
- import { Value } from '@sinclair/typebox/value';
1
+ import {
2
+ type PainSeverity as CorePainSeverity,
3
+ type PainSignal as CorePainSignal,
4
+ type PainSignalValidationResult as CorePainSignalValidationResult,
5
+ PainSeveritySchema,
6
+ PainSignalSchema,
7
+ deriveSeverity,
8
+ validatePainSignal,
9
+ } from '@principles/core/runtime-v2';
10
+
11
+ export type PainSeverity = CorePainSeverity;
12
+ export const PainSeverity = PainSeveritySchema;
13
+
14
+ export type PainSignal = CorePainSignal;
15
+ export type PainSignalValidationResult = CorePainSignalValidationResult;
16
+
17
+ export {
18
+ PainSignalSchema,
19
+ deriveSeverity,
20
+ validatePainSignal,
21
+ };
13
22
 
14
- // ---------------------------------------------------------------------------
15
- // PainSignal Schema
16
- // ---------------------------------------------------------------------------
17
-
18
- /**
19
- * Severity levels derived from pain score thresholds.
20
- * - low: 0-39 (minor issue, informational)
21
- * - medium: 40-69 (moderate error)
22
- * - high: 70-89 (severe violation)
23
- * - critical: 90-100 (systemic failure, spiral detected)
24
- */
25
- export const PainSeverity = Type.Union([
26
- Type.Literal('low'),
27
- Type.Literal('medium'),
28
- Type.Literal('high'),
29
- Type.Literal('critical'),
30
- ]);
31
-
32
- export type PainSeverity = Static<typeof PainSeverity>;
33
-
34
- /**
35
- * TypeBox schema for a universal pain signal.
36
- *
37
- * Every signal MUST have: source, score, timestamp, reason, sessionId,
38
- * agentId, traceId, triggerTextPreview. Optional fields (domain, severity,
39
- * context) default during validation.
40
- */
41
- export const PainSignalSchema = Type.Object({
42
- /** What triggered this pain signal (e.g., 'tool_failure', 'human_intervention') */
43
- source: Type.String({ minLength: 1 }),
44
- /** Pain score 0-100 */
45
- score: Type.Number({ minimum: 0, maximum: 100 }),
46
- /** ISO 8601 timestamp */
47
- timestamp: Type.String({ minLength: 1 }),
48
- /** Human-readable reason / error description */
49
- reason: Type.String({ minLength: 1 }),
50
- /** Session ID — identifies which conversation this happened in */
51
- sessionId: Type.Optional(Type.String()),
52
- /** Agent ID — identifies which agent (main, builder, diagnostician, etc.) */
53
- agentId: Type.Optional(Type.String()),
54
- /** Correlation trace ID for linking events across the pipeline */
55
- traceId: Type.Optional(Type.String()),
56
- /** Preview of the text that triggered this pain */
57
- triggerTextPreview: Type.String(),
58
- /** Domain context (e.g., 'coding', 'writing', 'analysis') */
59
- domain: Type.String({ default: 'coding' }),
60
- /** Severity level derived from score */
61
- severity: PainSeverity,
62
- /** Additional structured context payload */
63
- context: Type.Record(Type.String(), Type.Unknown()),
64
- });
65
-
66
- export type PainSignal = Static<typeof PainSignalSchema>;
67
-
68
- // ---------------------------------------------------------------------------
69
- // Default Derivation
70
- // ---------------------------------------------------------------------------
71
-
72
- /**
73
- * Derives severity from a numeric pain score.
74
- */
75
- export function deriveSeverity(score: number): PainSeverity {
76
- if (score >= 90) return 'critical';
77
- if (score >= 70) return 'high';
78
- if (score >= 40) return 'medium';
79
- return 'low';
80
- }
81
-
82
- // ---------------------------------------------------------------------------
83
- // Validation
84
- // ---------------------------------------------------------------------------
85
-
86
- export interface PainSignalValidationResult {
87
- valid: boolean;
88
- errors: string[];
89
- signal?: PainSignal;
90
- }
91
-
92
- /**
93
- * Validates an arbitrary object against the PainSignal schema.
94
- *
95
- * Returns a structured result with:
96
- * - `valid`: whether the input conforms to the schema
97
- * - `errors`: human-readable list of validation failures
98
- * - `signal`: the typed signal (only present when valid)
99
- *
100
- * Missing optional fields (domain, severity, context) are filled with defaults
101
- * before validation so callers get a fully-formed signal back.
102
- */
103
- export function validatePainSignal(input: unknown): PainSignalValidationResult {
104
- if (typeof input !== 'object' || input === null || Array.isArray(input)) {
105
- return { valid: false, errors: ['Input must be a non-null object'] };
106
- }
107
-
108
- const raw = input as Record<string, unknown>;
109
-
110
- // Apply defaults for optional fields before validation
111
- const hydrated = {
112
- ...raw,
113
- domain: raw.domain ?? 'coding',
114
- sessionId: raw.sessionId ?? undefined,
115
- agentId: raw.agentId ?? undefined,
116
- traceId: raw.traceId ?? undefined,
117
- severity: raw.severity ?? deriveSeverity(
118
- typeof raw.score === 'number' ? raw.score : 0,
119
- ),
120
- context: raw.context ?? {},
121
- };
122
-
123
- // Collect TypeBox errors
124
- const errors = [...Value.Errors(PainSignalSchema, hydrated)];
125
- if (errors.length > 0) {
126
- return {
127
- valid: false,
128
- errors: errors.map(
129
- (e) => `${e.path ? `${e.path}: ` : ''}${e.message}`,
130
- ),
131
- };
132
- }
133
-
134
- return {
135
- valid: true,
136
- errors: [],
137
- signal: Value.Cast(PainSignalSchema, hydrated) as PainSignal,
138
- };
139
- }
package/src/core/pain.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { serializeKvLines, parseKvLines, atomicWriteFileSync } from '../utils/io.js';
2
+ import { parseKvLines } from '../utils/io.js';
4
3
  import { resolvePdPath } from './paths.js';
5
4
  import { ConfigService } from './config-service.js';
6
5
  import { SystemLogger } from './system-logger.js';
7
6
 
8
7
  // =========================================================================
9
- // Pain Flag Contract (Single Source of Truth)
8
+ // Legacy Pain Flag Contract (Compatibility Only)
10
9
  //
11
- // All pain flag writers MUST use buildPainFlag() below.
12
- // If you need to add a new field, update this type AND buildPainFlag().
13
- // This prevents format drift across the 5+ pain signal sources.
10
+ // Runtime V2 pain diagnosis does NOT write .state/.pain_flag or consume it as
11
+ // an entry point. New pain signals must enter through emitPainDetectedEvent() or
12
+ // `pd pain record`, which both route to PainSignalBridge.
13
+ //
14
+ // The readers below remain only for legacy sleep-reflection context and
15
+ // historical state inspection. They must not trigger the diagnostician.
14
16
  // =========================================================================
15
17
 
16
18
  /**
@@ -47,15 +49,8 @@ export interface PainFlagContractResult {
47
49
  }
48
50
 
49
51
  /**
50
- * Factory function the ONLY way to construct pain flag data.
51
- *
52
- * All callers (hooks/pain.ts, hooks/subagent.ts, hooks/lifecycle.ts,
53
- * hooks/llm.ts, skills/pain/SKILL.md) must go through this function.
54
- *
55
- * Required fields are enforced by TypeScript — you can't compile
56
- * without providing source, score, time, reason, session_id, agent_id.
57
- *
58
- * Optional fields (trace_id, trigger_text_preview) default to empty string.
52
+ * Builds legacy pain flag data for compatibility tests and historical readers.
53
+ * Do not use this to create new Runtime V2 diagnosis requests.
59
54
  */
60
55
  export function buildPainFlag(input: {
61
56
  source: string;
@@ -153,71 +148,6 @@ export function painSeverityLabel(painScore: number, isSpiral = false, projectDi
153
148
  }
154
149
  }
155
150
 
156
- export function writePainFlag(projectDir: string, painData: PainFlagData): void {
157
- const painFlagPath = resolvePdPath(projectDir, 'PAIN_FLAG');
158
- const dir = path.dirname(painFlagPath);
159
- if (!fs.existsSync(dir)) {
160
- fs.mkdirSync(dir, { recursive: true });
161
- }
162
- atomicWriteFileSync(painFlagPath, serializeKvLines(painData));
163
- }
164
-
165
- /**
166
- * Combined trajectory record + pain flag write.
167
- *
168
- * Records the pain event to trajectory first to get the AUTOINCREMENT ID,
169
- * then builds and writes the pain flag with that ID embedded.
170
- * This guarantees the pain→principle→compile chain has the exact matching ID.
171
- *
172
- * Error handling: if trajectory write fails, continues without pain_event_id.
173
- * If flag write fails, the error propagates to the caller.
174
- */
175
- export function recordAndWritePainFlag(
176
- wctx: {
177
- trajectory?: { recordPainEvent(input: { sessionId: string; source: string; score: number; reason?: string | null; severity?: string | null; origin?: string | null; confidence?: number | null; text?: string }): number } | null;
178
- workspaceDir: string;
179
- },
180
- trajectoryParams: {
181
- sessionId: string;
182
- source: string;
183
- score: number;
184
- reason?: string | null;
185
- severity?: string | null;
186
- origin?: string | null;
187
- confidence?: number | null;
188
- text?: string;
189
- },
190
- painFlagParams: {
191
- source: string;
192
- score: string;
193
- reason: string;
194
- session_id?: string;
195
- agent_id?: string;
196
- is_risky?: boolean;
197
- trace_id?: string;
198
- trigger_text_preview?: string;
199
- }
200
- ): void {
201
- const trajectoryPainId = wctx.trajectory?.recordPainEvent({
202
- sessionId: trajectoryParams.sessionId,
203
- source: trajectoryParams.source,
204
- score: trajectoryParams.score,
205
- reason: trajectoryParams.reason ?? null,
206
- severity: trajectoryParams.severity ?? null,
207
- origin: trajectoryParams.origin ?? null,
208
- confidence: trajectoryParams.confidence ?? null,
209
- text: trajectoryParams.text,
210
- });
211
-
212
- const painData = buildPainFlag({
213
- ...painFlagParams,
214
- pain_event_id:
215
- trajectoryPainId !== undefined && trajectoryPainId >= 0 ? String(trajectoryPainId) : undefined,
216
- });
217
-
218
- writePainFlag(wctx.workspaceDir, painData);
219
- }
220
-
221
151
  /**
222
152
  * Converts a JSON pain flag object to KV format.
223
153
  */
@@ -252,10 +182,10 @@ function convertJsonToKv(json: Record<string, unknown>): Record<string, string>
252
182
  }
253
183
 
254
184
  /**
255
- * Reads and validates the pain flag file with auto-repair.
185
+ * Reads and validates the legacy pain flag file.
256
186
  *
257
187
  * - If file doesn't exist → returns {}
258
- * - If file is JSON format (wrong) → converts to KV, logs warning, rewrites file
188
+ * - If file is JSON format → converts to KV in memory only
259
189
  * - If file is KV format → validates required fields, logs warning if missing
260
190
  * - If file has unknown fields → silently ignores them (forward-compatible)
261
191
  */
@@ -270,7 +200,8 @@ export function readPainFlagData(projectDir: string): Record<string, string> {
270
200
  return {};
271
201
  }
272
202
 
273
- // Detect JSON format (wrong should be KV)
203
+ // Detect JSON format. Legacy compatibility only: parse in memory and do not
204
+ // rewrite .pain_flag, because Runtime V2 must not create or repair this file.
274
205
  if (content.startsWith('{')) {
275
206
 
276
207
  let json: Record<string, unknown>;
@@ -281,12 +212,8 @@ export function readPainFlagData(projectDir: string): Record<string, string> {
281
212
  return {};
282
213
  }
283
214
 
284
- // Auto-repair: convert JSON to KV format
285
215
  const kvData = convertJsonToKv(json);
286
-
287
- const repaired = serializeKvLines(kvData);
288
- atomicWriteFileSync(painFlagPath, repaired);
289
- SystemLogger.log(projectDir, 'PAIN_FLAG_AUTO_REPAIRED', `Auto-repaired pain flag from JSON to KV format (${Object.keys(json).length} fields)`);
216
+ SystemLogger.log(projectDir, 'PAIN_FLAG_LEGACY_JSON_READ', `Read legacy JSON pain flag in memory (${Object.keys(json).length} fields)`);
290
217
  return kvData;
291
218
  }
292
219
 
@@ -306,7 +306,6 @@ export class PathResolver {
306
306
  'THINKING_OS': workspacePath.join(workspace, '.principles', 'THINKING_OS.md'),
307
307
  'DECISION_POLICY': workspacePath.join(workspace, '.principles', 'DECISION_POLICY.json'),
308
308
  'MODELS_DIR': workspacePath.join(workspace, '.principles', 'models'),
309
- 'PLAN': workspacePath.join(workspace, 'PLAN.md'),
310
309
  'AGENT_SCORECARD': workspacePath.join(state, 'AGENT_SCORECARD.json'),
311
310
  'PAIN_FLAG': workspacePath.join(state, '.pain_flag'),
312
311
  'EVOLUTION_QUEUE': workspacePath.join(state, 'evolution_queue.json'),
package/src/core/paths.ts CHANGED
@@ -68,7 +68,6 @@ export const PD_FILES = {
68
68
  NOCTURNAL_EXPORTS_DIR: PD_DIRS.NOCTURNAL_EXPORTS,
69
69
  IMPL_CODE_DIR: PD_DIRS.IMPL_CODE_DIR,
70
70
 
71
- PLAN: 'PLAN.md',
72
71
  MEMORY_MD: 'MEMORY.md',
73
72
  HEARTBEAT: 'HEARTBEAT.md',
74
73
 
@@ -80,8 +80,7 @@ export interface ReconcileOptions {
80
80
  }
81
81
 
82
82
 
83
- async function readCronStore(logger?: { info?: (_: string) => void; warn?: (_: string) => void }): Promise<CronStoreFile> {
84
-
83
+ function readCronStore(logger?: { info?: (_: string) => void; warn?: (_: string) => void }): CronStoreFile {
85
84
  if (!fs.existsSync(CRON_STORE_PATH)) {
86
85
  logger?.info?.(`[PD:Reconciler] cron/jobs.json not found, starting with empty store`);
87
86
  return { version: 1, jobs: [] };
@@ -178,101 +177,7 @@ function buildCronJob(
178
177
  }
179
178
 
180
179
 
181
- function buildTaskPrompt(task: PDTaskSpec, workspaceDir: string, logger?: { info?: (_: string) => void }): string {
182
- // Resolve paths dynamically from workspaceDir instead of hardcoding
183
- const stateDir = path.join(workspaceDir, '.state');
184
- const empathyKeywordsPath = path.join(stateDir, 'empathy_keywords.json');
185
- const eventsJsonlPath = path.join(stateDir, 'logs', 'events.jsonl');
186
-
187
- if (task.id === 'empathy-optimizer') {
188
- logger?.info?.(`[PD:Reconciler] Building empathy optimizer prompt`);
189
- return `You are the Principles Disciple Empathy Keyword Optimizer.
190
-
191
- ## TASK
192
- Analyze the current empathy keyword store and recent user message logs to:
193
- 1. Discover NEW frustration expressions not in the current store
194
- 2. ADJUST weights of existing terms based on actual hit frequency
195
- 3. REMOVE terms that produce too many false positives
196
-
197
- ## WORKFLOW (execute in order)
198
-
199
- ### Step 1: Read current keyword store
200
- Use read_file to load:
201
- \`${empathyKeywordsPath}\`
202
-
203
- Examine the "terms" object. For each term note:
204
- - weight (0.1-0.9): higher = stronger frustration signal
205
- - hitCount: how many times it matched
206
- - falsePositiveRate (0.05-0.5): how often it's a false alarm
207
-
208
- ### Step 2: Read recent message logs
209
- Use search_file_content to scan:
210
- \`${eventsJsonlPath}\`
211
-
212
- Look for user messages containing frustration signals:
213
- - Negation: "不对", "错了", "不行", "重做"
214
- - Anger: "垃圾", "蠢", "废物", "白做"
215
- - Disappointment: "不行啊", "还是不对", "没解决"
216
- - Escalation: "你到底在干什么", "你确定吗", "what are you doing"
217
-
218
- ### Step 3: Write updated keyword store
219
- Use write_file to save the updated store back to:
220
- \`${empathyKeywordsPath}\`
221
-
222
- The file format is:
223
- \`\`\`json
224
- {
225
- "version": 1,
226
- "lastUpdated": "ISO timestamp",
227
- "lastOptimizedAt": "ISO timestamp",
228
- "terms": {
229
- "TERM": {
230
- "weight": 0.5,
231
- "source": "seed|llm_discovered|user_reported",
232
- "hitCount": 0,
233
- "falsePositiveRate": 0.15
234
- }
235
- },
236
- "stats": {
237
- "totalHits": 0,
238
- "totalFalsePositives": 0,
239
- "optimizationCount": 1
240
- }
241
- }
242
- \`\`\`
243
-
244
- **IMPORTANT**: You MUST use the write_file tool. Do NOT just return JSON in your response.
245
-
246
- ### Step 4: Report summary
247
- After writing the file, reply with a brief summary:
248
- \`\`\`
249
- Empathy keyword optimization complete:
250
- - Added: N new terms (list them)
251
- - Updated: M terms (list changes)
252
- - Removed: K terms (list them)
253
- - Total terms in store: X
254
- \`\`\`
255
-
256
- ## RULES
257
- - ADD: If you find frustration expressions in logs NOT in current terms
258
- - source = "llm_discovered", discoveredAt = current ISO timestamp
259
- - weight: 0.5-0.7 for new terms (start conservative)
260
- - falsePositiveRate: 0.2-0.3 (uncertain until validated)
261
- - UPDATE: Adjust based on evidence
262
- - High hitCount + low FPR → increase weight
263
- - Low hitCount + high FPR → decrease weight
264
- - Keep weight in 0.1-0.9, FPR in 0.05-0.5
265
- - REMOVE: If hitCount=0 AND falsePositiveRate > 0.3 AND term is clearly generic
266
- - Don't remove terms that might be valid but rare
267
- - PRESERVE: Keep existing hitCount, lastHitAt, discoveredAt for existing terms
268
- - Bump stats.optimizationCount by 1
269
- - Set lastOptimizedAt to current ISO timestamp
270
-
271
- ## EXAMPLES
272
- - "不对" has hitCount=50 → increase weight from 0.5 to 0.7
273
- - "呵呵" has hitCount=0, FPR=0.4, generic term → REMOVE
274
- - User says "烦死了" in logs, not in store → ADD weight=0.6, FPR=0.25`;
275
- }
180
+ function buildTaskPrompt(task: PDTaskSpec, _workspaceDir: string, _logger?: { info?: (_: string) => void }): string {
276
181
  return task.description;
277
182
  }
278
183
 
@@ -334,8 +239,10 @@ export async function reconcilePDTasks(
334
239
  case 'DISABLE':
335
240
  if (action.job) {
336
241
  if (!dryRun) {
337
- action.job.enabled = false;
338
- action.job.updatedAtMs = nowMs;
242
+ const idx = cronStore.jobs.indexOf(action.job);
243
+ if (idx >= 0) {
244
+ cronStore.jobs[idx] = { ...action.job, enabled: false, updatedAtMs: nowMs };
245
+ }
339
246
  }
340
247
  logger.warn?.(`[PD:Reconciler] Disabled job: ${action.job.name}`);
341
248
  }
@@ -381,9 +288,9 @@ function healthCheck(
381
288
  ): PDTaskSpec[] {
382
289
  const jobByName = new Map(cronStore.jobs.map((j) => [j.name, j]));
383
290
 
384
- for (const task of tasks) {
291
+ return tasks.map((task) => {
385
292
  const job = jobByName.get(task.name);
386
- if (!job) continue;
293
+ if (!job) return task;
387
294
 
388
295
  const errors = job.state.consecutiveErrors ?? 0;
389
296
  const lastError = job.state.lastError ?? '';
@@ -402,27 +309,31 @@ function healthCheck(
402
309
  }
403
310
 
404
311
  // Auto-disable only for non-timeout errors after 3 consecutive failures
405
- if (errors >= 3 && !isTimeout && !task.meta?.autoDisabled) {
406
- if (!task.meta) task.meta = {};
407
- task.meta.autoDisabled = true;
408
- task.meta.autoDisabledAt = Date.now();
409
- task.meta.autoDisabledReason = `consecutiveErrors=${errors}`;
410
- logger.warn?.(`[PD:Reconciler] Auto-disabled task '${task.id}' due to ${errors} consecutive errors: ${lastError.substring(0, 80)}`);
411
- }
312
+ const shouldAutoDisable = errors >= 3 && !isTimeout && !task.meta?.autoDisabled;
412
313
 
413
314
  // Reset consecutiveErrors on non-error runs
414
315
  if (errors === 0 && task.meta?.autoDisabled) {
415
316
  logger.info?.(`[PD:Reconciler] Task '${task.id}' was previously auto-disabled but has 0 consecutive errors now`);
416
317
  }
417
318
 
418
- if (task.meta) {
419
- task.meta.consecutiveFailCount = errors;
420
- if (job.state.lastRunAtMs) {
421
- task.meta.lastFailedAtMs = job.state.lastRunAtMs;
422
- }
319
+ if (shouldAutoDisable || task.meta || job.state.lastRunAtMs) {
320
+ return {
321
+ ...task,
322
+ meta: {
323
+ ...task.meta,
324
+ ...(shouldAutoDisable ? {
325
+ autoDisabled: true,
326
+ autoDisabledAt: Date.now(),
327
+ autoDisabledReason: `consecutiveErrors=${errors}`,
328
+ } : {}),
329
+ ...(task.meta ? { consecutiveFailCount: errors } : {}),
330
+ ...(job.state.lastRunAtMs ? { lastFailedAtMs: job.state.lastRunAtMs } : {}),
331
+ },
332
+ };
423
333
  }
424
- }
425
- return tasks;
334
+
335
+ return task;
336
+ });
426
337
  }
427
338
 
428
339
  export async function trigger(