principles-disciple 1.72.0 → 1.73.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 (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  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 +38 -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/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +459 -439
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +59 -16
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +3 -1
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. package/ui/src/utils/format.ts +0 -15
@@ -30,11 +30,10 @@
30
30
  * - Observations are retained until cleanup removes expired entries
31
31
  */
32
32
 
33
- import * as fs from 'fs';
34
33
  import * as path from 'path';
35
34
  import * as crypto from 'crypto';
36
35
  import { withLock } from '../utils/file-lock.js';
37
- import { atomicWriteFileSync } from '../utils/io.js';
36
+ import { JsonFileStore } from './file-store.js';
38
37
 
39
38
  // ---------------------------------------------------------------------------
40
39
  // Constants
@@ -170,67 +169,44 @@ export interface ShadowRegistry {
170
169
  }
171
170
 
172
171
  // ---------------------------------------------------------------------------
173
- // Registry Path
172
+ // Registry Path & Store
174
173
  // ---------------------------------------------------------------------------
175
174
 
175
+ let _store: Map<string, JsonFileStore<ShadowRegistry>> = new Map();
176
+
176
177
  function getRegistryPath(stateDir: string): string {
177
178
  return path.join(stateDir, SHADOW_REGISTRY_FILE);
178
179
  }
179
180
 
180
- /**
181
- * Ensure the registry directory exists.
182
- */
183
- function ensureRegistryDir(stateDir: string): void {
184
- const registryPath = getRegistryPath(stateDir);
185
- const dir = path.dirname(registryPath);
186
- if (!fs.existsSync(dir)) {
187
- fs.mkdirSync(dir, { recursive: true });
181
+ function getStore(stateDir: string): JsonFileStore<ShadowRegistry> {
182
+ let store = _store.get(stateDir);
183
+ if (!store) {
184
+ const filePath = path.join(stateDir, SHADOW_REGISTRY_FILE);
185
+ store = new JsonFileStore<ShadowRegistry>(filePath, () => ({ observations: [], version: 1 }));
186
+ _store.set(stateDir, store);
188
187
  }
188
+ return store;
189
189
  }
190
190
 
191
191
  // ---------------------------------------------------------------------------
192
192
  // File Operations
193
193
  // ---------------------------------------------------------------------------
194
194
 
195
- /**
196
- * Read the registry from disk. Returns empty registry if missing.
197
- */
198
- function readRegistry(stateDir: string): ShadowRegistry {
199
- const registryPath = getRegistryPath(stateDir);
200
- if (!fs.existsSync(registryPath)) {
201
- return { observations: [], version: 1 };
202
- }
203
- try {
204
- const content = fs.readFileSync(registryPath, 'utf-8');
205
- return JSON.parse(content) as ShadowRegistry;
206
- } catch (err) {
207
- console.warn(`[shadow-observation-registry] Registry corrupted at ${registryPath}, recovering with empty state: ${String(err)}`);
208
- return { observations: [], version: 1 };
209
- }
210
- }
211
-
212
- /**
213
- * Write the registry to disk atomically.
214
- */
215
- function writeRegistry(stateDir: string, registry: ShadowRegistry): void {
216
- ensureRegistryDir(stateDir);
217
- const registryPath = getRegistryPath(stateDir);
218
- atomicWriteFileSync(registryPath, JSON.stringify(registry, null, 2));
219
- }
220
-
221
195
  /**
222
196
  * Execute a read-modify-write under an exclusive file lock.
223
197
  */
224
-
225
198
  function withShadowRegistryLock<T>(
226
199
  stateDir: string,
227
200
  fn: (_registry: ShadowRegistry) => T
228
201
  ): T {
229
-
230
- const registryPath = getRegistryPath(stateDir);
231
- return withLock(registryPath, () => {
232
- const registry = readRegistry(stateDir);
233
- return fn(registry);
202
+ const store = getStore(stateDir);
203
+ const filePath = getRegistryPath(stateDir);
204
+ // Re-create lock behavior using the store's mutate under a simple fs-based lock
205
+ return withLock(filePath, () => {
206
+ const registry = store.load();
207
+ const result = fn(registry);
208
+ store.save(registry);
209
+ return result;
234
210
  });
235
211
  }
236
212
 
@@ -276,7 +252,6 @@ export function recordShadowRouting(
276
252
 
277
253
  return withShadowRegistryLock(stateDir, (registry) => {
278
254
  registry.observations.push(observation);
279
- writeRegistry(stateDir, registry);
280
255
  return observation;
281
256
  });
282
257
  }
@@ -326,7 +301,6 @@ export function completeShadowObservation(
326
301
  extra: {},
327
302
  };
328
303
 
329
- writeRegistry(stateDir, registry);
330
304
  return observation;
331
305
  });
332
306
  }
@@ -369,7 +343,6 @@ export function completeShadowObservationByTask(
369
343
  extra: {},
370
344
  };
371
345
 
372
- writeRegistry(stateDir, registry);
373
346
  return observation;
374
347
  });
375
348
  }
@@ -506,7 +479,6 @@ export function markObservationsUsedInGate(
506
479
  obs.usedInGate = true;
507
480
  }
508
481
  }
509
- writeRegistry(stateDir, registry);
510
482
  });
511
483
  }
512
484
 
@@ -530,7 +502,6 @@ export function cleanupExpiredObservations(
530
502
  (o) => o.routedAt >= cutoff
531
503
  );
532
504
  removed = before - registry.observations.length;
533
- writeRegistry(stateDir, registry);
534
505
  });
535
506
 
536
507
  return removed;
@@ -861,12 +861,13 @@ export class TrajectoryDatabase {
861
861
  listUserTurnsForSession(sessionId: string): {
862
862
  id: number;
863
863
  turnIndex: number;
864
+ rawExcerpt: string;
864
865
  correctionDetected: boolean;
865
866
  correctionCue: string | null;
866
867
  createdAt: string;
867
868
  }[] {
868
869
  const rows = this.db.prepare(`
869
- SELECT id, turn_index, correction_detected, correction_cue, created_at
870
+ SELECT id, turn_index, raw_excerpt, correction_detected, correction_cue, created_at
870
871
  FROM user_turns
871
872
  WHERE session_id = ?
872
873
  ORDER BY turn_index ASC
@@ -875,6 +876,7 @@ export class TrajectoryDatabase {
875
876
  return rows.map((row) => ({
876
877
  id: Number(row.id),
877
878
  turnIndex: Number(row.turn_index),
879
+ rawExcerpt: String(row.raw_excerpt ?? ''),
878
880
  correctionDetected: Boolean(row.correction_detected),
879
881
  correctionCue: row.correction_cue ? String(row.correction_cue) : null,
880
882
  createdAt: String(row.created_at),
@@ -18,16 +18,34 @@ import yaml from 'js-yaml';
18
18
 
19
19
  /**
20
20
  * A single stage in a workflow funnel.
21
+ * Policy fields are optional — existing stages without policy fields remain valid.
21
22
  */
22
23
  export interface WorkflowStage {
23
- /** Stage name within the funnel (e.g., 'dreamer_completed') */
24
24
  name: string;
25
- /** Event type string (e.g., 'nocturnal_dreamer_completed') */
26
25
  eventType: string;
27
- /** Event category (e.g., 'completed', 'created', 'blocked') */
28
26
  eventCategory: string;
29
- /** Dot-path to stats field (e.g., 'evolution.nocturnalDreamerCompleted') */
30
27
  statsField: string;
28
+ timeoutMs?: number;
29
+ successCriteria?: string;
30
+ legacyDisabled?: boolean;
31
+ observability?: {
32
+ enabled?: boolean;
33
+ emitEvents?: string[];
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Funnel-level policy that applies to all stages unless overridden per-stage.
39
+ */
40
+ export interface FunnelPolicy {
41
+ timeoutMs?: number;
42
+ stageOrder?: 'strict' | 'relaxed';
43
+ legacyDisabled?: boolean;
44
+ observability?: {
45
+ enabled?: boolean;
46
+ emitEvents?: string[];
47
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
48
+ };
31
49
  }
32
50
 
33
51
  /**
@@ -36,6 +54,7 @@ export interface WorkflowStage {
36
54
  export interface WorkflowFunnel {
37
55
  workflowId: string;
38
56
  stages: WorkflowStage[];
57
+ policy?: FunnelPolicy;
39
58
  }
40
59
 
41
60
  /**
@@ -50,56 +69,31 @@ export interface WorkflowFunnelConfig {
50
69
  // WorkflowFunnelLoader
51
70
  // ─────────────────────────────────────────────────────────────────────────────
52
71
 
53
- /**
54
- * Loads and watches workflows.yaml, building an in-memory WORKFLOW_FUNNELS table.
55
- *
56
- * Failure semantics (per Codex review):
57
- * - Missing file: clears in-memory funnels, uses empty Map
58
- * - Malformed YAML: preserves last known-good config, logs warning
59
- * - Schema-invalid YAML: same as malformed YAML
60
- *
61
- * Usage:
62
- * const loader = new WorkflowFunnelLoader(stateDir);
63
- * const funnels = loader.getAllFunnels(); // Map<string, WorkflowStage[]>
64
- * loader.watch(); // Enable hot reload
65
- */
66
72
  export class WorkflowFunnelLoader {
67
- /** In-memory WORKFLOW_FUNNELS table: workflowId -> stages */
68
73
  private readonly funnels = new Map<string, WorkflowStage[]>();
69
-
74
+ private readonly fullFunnels = new Map<string, WorkflowFunnel>();
70
75
  private readonly configPath: string;
71
-
72
- /** fs.watch() handle for cleanup */
73
76
  private watchHandle?: fs.FSWatcher;
74
-
75
- /** YAML parse warnings from last load() call */
76
77
  private readonly warnings: string[] = [];
77
78
 
78
79
  constructor(stateDir: string) {
79
- // D-02: workflows.yaml in .state/ directory
80
80
  this.configPath = path.join(stateDir, 'workflows.yaml');
81
81
  this.load();
82
82
  }
83
83
 
84
- /**
85
- * Load (or reload) workflows.yaml from disk.
86
- * On parse/validation failure, preserves the last known-good config.
87
- * On missing file, clears to empty.
88
- */
89
84
  load(): void {
90
- this.warnings.length = 0; // reset warnings on each load
85
+ this.warnings.length = 0;
91
86
  if (!fs.existsSync(this.configPath)) {
92
87
  this.warnings.push('workflows.yaml file not found.');
93
88
  this.funnels.clear();
89
+ this.fullFunnels.clear();
94
90
  return;
95
91
  }
96
92
 
97
93
  try {
98
94
  const content = fs.readFileSync(this.configPath, 'utf-8');
99
- // Use safe load — no arbitrary code execution
100
95
  const config = yaml.load(content, { schema: yaml.DEFAULT_SCHEMA }) as WorkflowFunnelConfig;
101
96
 
102
- // Validate top-level structure
103
97
  if (!config || typeof config.version !== 'string' || !Array.isArray(config.funnels)) {
104
98
  const msg = 'workflows.yaml validation failed: missing version or funnels array. Preserving last valid config.';
105
99
  console.warn(`[WorkflowFunnelLoader] ${msg}`);
@@ -107,11 +101,12 @@ export class WorkflowFunnelLoader {
107
101
  return;
108
102
  }
109
103
 
110
- // Rebuild funnels map
111
104
  const newFunnels = new Map<string, WorkflowStage[]>();
105
+ const newFullFunnels = new Map<string, WorkflowFunnel>();
112
106
  for (const funnel of config.funnels) {
113
107
  if (funnel?.workflowId && typeof funnel.workflowId === 'string' && Array.isArray(funnel.stages)) {
114
108
  newFunnels.set(funnel.workflowId, funnel.stages);
109
+ newFullFunnels.set(funnel.workflowId, funnel);
115
110
  } else {
116
111
  const msg = 'Skipping invalid funnel entry: missing workflowId or stages.';
117
112
  console.warn(`[WorkflowFunnelLoader] ${msg}`);
@@ -119,44 +114,28 @@ export class WorkflowFunnelLoader {
119
114
  }
120
115
  }
121
116
 
122
- // Atomic replace: only commit if entire parse/validation succeeded
123
117
  this.funnels.clear();
124
- for (const [k, v] of newFunnels) {
125
- this.funnels.set(k, v);
126
- }
118
+ this.fullFunnels.clear();
119
+ for (const [k, v] of newFunnels) { this.funnels.set(k, v); }
120
+ for (const [k, v] of newFullFunnels) { this.fullFunnels.set(k, v); }
127
121
  } catch (err) {
128
- // Best-effort: preserve last known-good config on parse error
129
122
  const msg = `Failed to parse workflows.yaml: ${String(err)}. Preserving last valid config.`;
130
123
  console.warn(`[WorkflowFunnelLoader] ${msg}`);
131
124
  this.warnings.push(msg);
132
125
  }
133
126
  }
134
127
 
135
- /**
136
- * Start watching workflows.yaml for changes.
137
- * Calls load() automatically when the file changes.
138
- * No-op if the config file does not exist.
139
- */
140
128
  watch(): void {
141
- // WATCHER-01: re-entry guard — prevent FSWatcher leak on double-watch
142
129
  if (this.watchHandle) return;
143
- // Guard: fs.watch fails with ENOENT if the path does not exist
144
130
  if (!fs.existsSync(this.configPath)) return;
145
- // Debounce: only re-read after file write settles (100ms)
146
131
  let debounceTimer: ReturnType<typeof setTimeout> | undefined;
147
132
  this.watchHandle = fs.watch(this.configPath, (eventType) => {
148
- // PLAT-01: handle both 'change' and 'rename' events for Windows compatibility
149
133
  if (eventType !== 'change' && eventType !== 'rename') return;
150
134
  if (debounceTimer) clearTimeout(debounceTimer);
151
- debounceTimer = setTimeout(() => {
152
- this.load();
153
- }, 100);
135
+ debounceTimer = setTimeout(() => this.load(), 100);
154
136
  });
155
137
  }
156
138
 
157
- /**
158
- * Stop watching and clean up the FSWatcher.
159
- */
160
139
  dispose(): void {
161
140
  if (this.watchHandle) {
162
141
  this.watchHandle.close();
@@ -164,38 +143,53 @@ export class WorkflowFunnelLoader {
164
143
  }
165
144
  }
166
145
 
167
- /**
168
- * Get all stages for a workflow.
169
- */
170
146
  getStages(workflowId: string): WorkflowStage[] {
171
147
  return this.funnels.get(workflowId) ?? [];
172
148
  }
173
149
 
174
- /**
175
- * Get the full WORKFLOW_FUNNELS table.
176
- * Returns a deep clone — consumer mutations do not affect internal state.
177
- */
150
+ getFunnel(workflowId: string): WorkflowFunnel | undefined {
151
+ const funnel = this.fullFunnels.get(workflowId);
152
+ if (!funnel) return undefined;
153
+ return this.cloneFunnel(funnel);
154
+ }
155
+
178
156
  getAllFunnels(): Map<string, WorkflowStage[]> {
179
157
  const result = new Map<string, WorkflowStage[]>();
180
158
  for (const [k, v] of this.funnels) {
181
- // WATCHER-03: deep-clone arrays and stage objects
182
159
  result.set(k, v.map(stage => ({ ...stage })));
183
160
  }
184
161
  return result;
185
162
  }
186
163
 
187
- /**
188
- * Returns warnings from the last load() call.
189
- * Callers can inspect these and propagate them to metadata.warnings.
190
- */
164
+ getAllFunnelsWithPolicy(): Map<string, WorkflowFunnel> {
165
+ const result = new Map<string, WorkflowFunnel>();
166
+ for (const [k, v] of this.fullFunnels) {
167
+ result.set(k, this.cloneFunnel(v));
168
+ }
169
+ return result;
170
+ }
171
+
191
172
  getWarnings(): string[] {
192
173
  return [...this.warnings];
193
174
  }
194
175
 
195
- /**
196
- * Get the config file path (for testing/debugging).
197
- */
198
176
  getConfigPath(): string {
199
177
  return this.configPath;
200
178
  }
179
+
180
+ private cloneFunnel(funnel: WorkflowFunnel): WorkflowFunnel {
181
+ return {
182
+ ...funnel,
183
+ stages: funnel.stages.map(stage => ({ ...stage })),
184
+ policy: funnel.policy ? {
185
+ ...funnel.policy,
186
+ observability: funnel.policy.observability ? {
187
+ ...funnel.policy.observability,
188
+ emitEvents: funnel.policy.observability.emitEvents
189
+ ? [...funnel.policy.observability.emitEvents]
190
+ : undefined,
191
+ } : undefined,
192
+ } : undefined,
193
+ };
194
+ }
201
195
  }
@@ -1,6 +1,7 @@
1
1
  import type { PD_FILES } from './paths.js';
2
2
  import { resolvePdPath } from './paths.js';
3
3
  import { PathResolver } from './path-resolver.js';
4
+ import { validateWorkspaceDir } from './workspace-dir-validation.js';
4
5
  import { ConfigService } from './config-service.js';
5
6
  import type { PainConfig } from './config.js';
6
7
  import type { EventLog } from './event-log.js';
@@ -193,6 +194,14 @@ export class WorkspaceContext {
193
194
  }
194
195
  }
195
196
 
197
+ const validationIssue = validateWorkspaceDir(workspaceDir);
198
+ if (validationIssue !== null) {
199
+ logWarn(
200
+ `[PD:WorkspaceContext] LEGACY_PATH_RESOLVER_FALLBACK: ${validationIssue}. ` +
201
+ 'This is a legacy discovery path; explicit workspaceDir should be provided in the hook context.',
202
+ );
203
+ }
204
+
196
205
  const existing = this.instances.get(workspaceDir);
197
206
  if (existing) return existing;
198
207
 
@@ -210,6 +219,43 @@ export class WorkspaceContext {
210
219
  return instance;
211
220
  }
212
221
 
222
+ /**
223
+ * Creates a WorkspaceContext requiring explicit workspaceDir.
224
+ * For Runtime V2 entrypoints where implicit PathResolver fallback is unacceptable.
225
+ * @throws Error if workspaceDir is not provided in the context.
226
+ */
227
+ static fromHookContextExplicit(ctx: { workspaceDir?: string; logger?: { error?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void; info?: (...args: unknown[]) => void } }): WorkspaceContext {
228
+ const { logger } = ctx;
229
+ let { workspaceDir } = ctx;
230
+ if (!workspaceDir || !workspaceDir.trim()) {
231
+ const error = {
232
+ ok: false as const,
233
+ reason: 'workspace_dir_missing',
234
+ message: 'workspaceDir is required for Runtime V2 entrypoints. Provide it explicitly in the hook context.',
235
+ nextAction: 'Ensure the OpenClaw hook context includes workspaceDir, or use PD_WORKSPACE_DIR env var.',
236
+ };
237
+ logger?.error?.(`[PD:WorkspaceContext] ${error.message}`);
238
+ throw new Error(`[PD:WorkspaceContext] ${error.reason}: ${error.message}`);
239
+ }
240
+ const normalized = this.pathResolver.normalizeWorkspacePath(workspaceDir);
241
+ if (normalized !== workspaceDir) {
242
+ logger?.info?.(`[PD:WorkspaceContext] Normalized workspaceDir before validation: ${workspaceDir} -> ${normalized}`);
243
+ workspaceDir = normalized;
244
+ }
245
+ const validationIssue = validateWorkspaceDir(workspaceDir);
246
+ if (validationIssue !== null) {
247
+ const error = {
248
+ ok: false as const,
249
+ reason: 'workspace_dir_invalid',
250
+ message: `workspaceDir validation failed for Runtime V2 entrypoint: ${validationIssue}`,
251
+ nextAction: 'Provide a valid workspaceDir that is not the home directory, root, or empty.',
252
+ };
253
+ logger?.error?.(`[PD:WorkspaceContext] ${error.message}`);
254
+ throw new Error(`[PD:WorkspaceContext] ${error.reason}: ${error.message}`);
255
+ }
256
+ return this.fromHookContext({ ...ctx, workspaceDir });
257
+ }
258
+
213
259
  /**
214
260
  * Resolves a PD file path within the workspace.
215
261
  */
@@ -19,7 +19,7 @@ function tryResolveFromAgent(
19
19
  attempts: string[],
20
20
  ): string | undefined {
21
21
  try {
22
- const resolved = api.runtime.agent.resolveAgentWorkspaceDir(api.config, agentId);
22
+ const resolved = api.runtime?.agent?.resolveAgentWorkspaceDir?.(api.config, agentId);
23
23
  const issue = validateWorkspaceDir(resolved);
24
24
  if (!issue) {
25
25
  return resolved;
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * WorkspaceDir Validation Utilities
3
3
  *
4
4
  * This module only validates candidate workspace directories and delegates
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import * as os from 'os';
9
+ import * as path from 'path';
9
10
 
10
11
  export interface WorkspaceResolutionContext {
11
12
  workspaceDir?: string;
@@ -17,27 +18,35 @@ export function validateWorkspaceDir(dir: string | undefined): string | null {
17
18
  return 'workspaceDir is undefined/null';
18
19
  }
19
20
 
21
+ if (/^[A-Za-z]:\\?$/.test(dir)) {
22
+ return `workspaceDir is a drive root: "${dir}"`;
23
+ }
24
+
25
+ const resolved = path.resolve(dir);
20
26
  const homeDir = os.homedir();
21
27
 
22
- if (dir === homeDir) {
28
+ if (resolved === homeDir) {
23
29
  return `workspaceDir equals home directory (${homeDir}), likely missing context field`;
24
30
  }
25
31
 
26
- if (dir === '/' || dir === '') {
27
- return `workspaceDir is root or empty: "${dir}"`;
32
+ if (resolved === '/' || resolved === '') {
33
+ return `workspaceDir is root or empty: "${resolved}"`;
34
+ }
35
+
36
+ if (/^[A-Za-z]:\\?$/.test(resolved)) {
37
+ return `workspaceDir is a drive root: "${resolved}"`;
28
38
  }
29
39
 
30
40
  const escapedHome = homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
41
  const badPatterns = [
32
- { pattern: new RegExp(`^${escapedHome}$`), desc: 'is home directory itself' },
33
- { pattern: new RegExp(`^${escapedHome}/$`), desc: 'is home directory with trailing slash' },
42
+ { pattern: new RegExp(`^${escapedHome}[\\\\/]?$`), desc: 'is home directory' },
34
43
  ];
35
44
 
36
45
  for (const { pattern, desc } of badPatterns) {
37
- if (pattern.test(dir)) {
38
- return `workspaceDir ${desc}: "${dir}"`;
46
+ if (pattern.test(resolved)) {
47
+ return `workspaceDir ${desc}: "${resolved}"`;
39
48
  }
40
49
  }
41
50
 
42
51
  return null;
43
- }
52
+ }
@@ -8,7 +8,7 @@
8
8
  |------------|------|--------------|
9
9
  | `before_prompt_build` | `prompt.ts` | Multi-layer context injection: identity, trust, evolution, principles, thinking OS |
10
10
  | `before_tool_call` | `gate.ts` | Security gate: trust stage checks, risk path blocking, bash security (Cyrillic de-obfuscation, command tokenization) |
11
- | `after_tool_call` | `pain.ts` | Pain detection: failure → pain score → `.pain_flag` evolution queue |
11
+ | `after_tool_call` | `pain.ts` | Pain detection: failure → pain score → Runtime V2 `PainSignalBridge` |
12
12
  | `before_compaction` | `lifecycle.ts` | Checkpoints state before context loss |
13
13
  | `after_compaction` | `lifecycle.ts` | State recovery |
14
14
  | `before_reset` / `session_*` | `lifecycle.ts` | Session lifecycle management |
@@ -10,14 +10,15 @@
10
10
  * had their own block persistence implementations.
11
11
  */
12
12
 
13
- import { trackBlock } from '../core/session-tracker.js';
13
+ import { getSession, trackBlock } from '../core/session-tracker.js';
14
14
  import type { WorkspaceContext } from '../core/workspace-context.js';
15
15
  import type { PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
16
+ import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
17
+ import { emitPainDetectedEvent } from './pain.js';
16
18
  import {
17
19
  TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS,
18
20
  TRAJECTORY_GATE_BLOCK_MAX_RETRIES
19
21
  } from '../config/index.js';
20
- import { buildPainFlag, writePainFlag } from '../core/pain.js';
21
22
 
22
23
  /**
23
24
  * Block context containing all information needed for block persistence
@@ -99,51 +100,52 @@ export function recordGateBlockAndReturn(
99
100
  scheduleTrajectoryGateBlockRetry(wctx, trajectoryPayload, 1, logWarn, logError);
100
101
  }
101
102
 
102
- // 5. Emit pain signal for gate block (#256)
103
- // Gate blocks are a strong frustration signal the agent tried to do something
104
- // and was blocked by a principle gate. This should feed into the nocturnal pipeline.
103
+ // 5. Record gate block pain context. Runtime V2 diagnosis is gated by GFI
104
+ // so one mild block does not start a long diagnostician run.
105
105
  if (sessionId) {
106
- const GATE_BLOCK_PAIN_SCORE = 30; // Moderate not a failure but a blocked intent
107
- try {
108
- const trajectoryPainId = wctx.trajectory?.recordPainEvent?.({
109
- sessionId,
110
- source: 'gate_blocked',
111
- score: GATE_BLOCK_PAIN_SCORE,
112
- reason: `Gate blocked ${toolName} on ${filePath}: ${reason}`,
113
- severity: 'mild',
114
- origin: 'system_infer',
115
- });
106
+ const GATE_BLOCK_PAIN_SCORE = 45; // Must be >= pain_trigger (40) so single gate block can trigger diagnosis (PRI-274)
107
+ // Record to trajectory (fire-and-forget, no .pain_flag file needed)
108
+ wctx.trajectory?.recordPainEvent?.({
109
+ sessionId,
110
+ source: 'gate_blocked',
111
+ score: GATE_BLOCK_PAIN_SCORE,
112
+ reason: `Gate blocked ${toolName} on ${filePath}: ${reason}`,
113
+ severity: 'mild',
114
+ origin: 'system_infer',
115
+ });
116
116
 
117
- // Update .pain_flag if score is significant
118
- wctx.eventLog.recordPainSignal(sessionId, {
119
- source: 'gate_blocked',
120
- score: GATE_BLOCK_PAIN_SCORE,
121
- reason,
122
- });
117
+ const session = getSession(sessionId);
118
+ const gate = evaluatePainDiagnosticGate({
119
+ source: 'gate_blocked',
120
+ score: GATE_BLOCK_PAIN_SCORE,
121
+ currentGfi: session?.currentGfi ?? 0,
122
+ consecutiveErrors: session?.consecutiveErrors ?? 0,
123
+ sessionId,
124
+ errorHash: `${toolName}:${filePath}:${reason}`,
125
+ thresholds: {
126
+ painTrigger: wctx.config.get('thresholds.pain_trigger') || 40,
127
+ highSeverity: wctx.config.get('severity_thresholds.high') || 70,
128
+ },
129
+ });
123
130
 
124
- // Write to pain flag file (merge with existing if present)
125
- try {
126
-
127
- const workspaceDir = wctx.workspaceDir;
128
- const currentFlag = wctx.eventLog.findLatestPainSignal(sessionId);
129
- const currentScore = currentFlag?.score ?? 0;
130
- if (currentScore < GATE_BLOCK_PAIN_SCORE) {
131
- const flag = buildPainFlag({
132
- source: 'gate_blocked',
133
- score: String(GATE_BLOCK_PAIN_SCORE),
134
- reason: `Gate blocked: ${reason}`,
135
- session_id: sessionId,
136
- agent_id: 'main',
137
- is_risky: false,
138
- pain_event_id: trajectoryPainId !== undefined && trajectoryPainId >= 0 ? String(trajectoryPainId) : undefined,
139
- });
140
- writePainFlag(workspaceDir, flag);
141
- }
142
- } catch (flagErr) {
143
- logWarn(`[PD_GATE] Failed to update pain flag for gate block: ${String(flagErr)}`);
144
- }
145
- } catch (painErr) {
146
- logWarn(`[PD_GATE] Failed to record gate block pain signal: ${String(painErr)}`);
131
+ if (gate.shouldDiagnose) {
132
+ void emitPainDetectedEvent(wctx, {
133
+ ts: new Date().toISOString(),
134
+ type: 'pain_detected',
135
+ data: {
136
+ painId: `gate_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
137
+ painType: 'user_frustration',
138
+ source: 'gate_blocked',
139
+ reason: `Gate blocked ${toolName} on ${filePath}: ${reason}`,
140
+ score: GATE_BLOCK_PAIN_SCORE,
141
+ sessionId,
142
+ agentId: 'main',
143
+ },
144
+ }).catch((emitErr) => {
145
+ logWarn(`[PD_GATE] Failed to emit gate block pain event: ${String(emitErr)}`);
146
+ });
147
+ } else {
148
+ logger.info?.(`[PD_GATE] Gate block recorded without Runtime V2 diagnosis: ${gate.detail}`);
147
149
  }
148
150
  }
149
151