principles-disciple 1.11.0 → 1.13.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 (233) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +66 -0
  3. package/esbuild.config.js +1 -1
  4. package/openclaw.plugin.json +4 -4
  5. package/package.json +2 -3
  6. package/run-nocturnal.mjs +30 -0
  7. package/scripts/db-migrate.mjs +170 -0
  8. package/scripts/sync-plugin.mjs +250 -6
  9. package/src/commands/archive-impl.ts +136 -0
  10. package/src/commands/capabilities.ts +4 -2
  11. package/src/commands/context.ts +5 -1
  12. package/src/commands/disable-impl.ts +151 -0
  13. package/src/commands/evolution-status.ts +64 -19
  14. package/src/commands/export.ts +8 -6
  15. package/src/commands/focus.ts +8 -20
  16. package/src/commands/nocturnal-review.ts +5 -7
  17. package/src/commands/nocturnal-rollout.ts +1 -12
  18. package/src/commands/nocturnal-train.ts +21 -47
  19. package/src/commands/pain.ts +10 -5
  20. package/src/commands/principle-rollback.ts +4 -2
  21. package/src/commands/promote-impl.ts +274 -0
  22. package/src/commands/rollback-impl.ts +234 -0
  23. package/src/commands/rollback.ts +6 -3
  24. package/src/commands/samples.ts +2 -0
  25. package/src/commands/thinking-os.ts +3 -4
  26. package/src/commands/workflow-debug.ts +2 -1
  27. package/src/config/errors.ts +1 -0
  28. package/src/core/AGENTS.md +34 -0
  29. package/src/core/adaptive-thresholds.ts +4 -3
  30. package/src/core/code-implementation-storage.ts +241 -0
  31. package/src/core/config.ts +5 -2
  32. package/src/core/control-ui-db.ts +29 -10
  33. package/src/core/detection-funnel.ts +12 -7
  34. package/src/core/diagnostician-task-store.ts +156 -0
  35. package/src/core/dictionary.ts +4 -4
  36. package/src/core/empathy-keyword-matcher.ts +7 -3
  37. package/src/core/empathy-types.ts +13 -2
  38. package/src/core/event-log.ts +14 -6
  39. package/src/core/evolution-engine.ts +27 -31
  40. package/src/core/evolution-logger.ts +3 -2
  41. package/src/core/evolution-reducer.ts +110 -31
  42. package/src/core/evolution-types.ts +10 -0
  43. package/src/core/external-training-contract.ts +1 -0
  44. package/src/core/focus-history.ts +38 -24
  45. package/src/core/hygiene/tracker.ts +10 -6
  46. package/src/core/init.ts +5 -2
  47. package/src/core/migration.ts +3 -3
  48. package/src/core/model-deployment-registry.ts +6 -4
  49. package/src/core/model-training-registry.ts +5 -3
  50. package/src/core/nocturnal-arbiter.ts +13 -14
  51. package/src/core/nocturnal-artifact-lineage.ts +117 -0
  52. package/src/core/nocturnal-artificer.ts +257 -0
  53. package/src/core/nocturnal-candidate-scoring.ts +4 -2
  54. package/src/core/nocturnal-compliance.ts +67 -19
  55. package/src/core/nocturnal-dataset.ts +95 -2
  56. package/src/core/nocturnal-executability.ts +2 -3
  57. package/src/core/nocturnal-export.ts +6 -3
  58. package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
  59. package/src/core/nocturnal-trajectory-extractor.ts +10 -3
  60. package/src/core/nocturnal-trinity.ts +319 -61
  61. package/src/core/pain-context-extractor.ts +29 -15
  62. package/src/core/pain.ts +7 -5
  63. package/src/core/path-resolver.ts +16 -15
  64. package/src/core/paths.ts +2 -1
  65. package/src/core/pd-task-reconciler.ts +463 -0
  66. package/src/core/pd-task-service.ts +42 -0
  67. package/src/core/pd-task-store.ts +77 -0
  68. package/src/core/pd-task-types.ts +128 -0
  69. package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
  70. package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
  71. package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
  72. package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
  73. package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
  74. package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
  75. package/src/core/principle-training-state.ts +95 -370
  76. package/src/core/principle-tree-ledger.ts +733 -0
  77. package/src/core/principle-tree-migration.ts +195 -0
  78. package/src/core/profile.ts +3 -1
  79. package/src/core/promotion-gate.ts +14 -18
  80. package/src/core/replay-engine.ts +562 -0
  81. package/src/core/risk-calculator.ts +6 -4
  82. package/src/core/rule-host-helpers.ts +39 -0
  83. package/src/core/rule-host-types.ts +82 -0
  84. package/src/core/rule-host.ts +245 -0
  85. package/src/core/rule-implementation-runtime.ts +38 -0
  86. package/src/core/schema/db-types.ts +16 -0
  87. package/src/core/schema/index.ts +26 -0
  88. package/src/core/schema/migration-runner.ts +207 -0
  89. package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
  90. package/src/core/schema/migrations/002-init-central.ts +122 -0
  91. package/src/core/schema/migrations/003-init-workflow.ts +55 -0
  92. package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
  93. package/src/core/schema/migrations/index.ts +31 -0
  94. package/src/core/schema/schema-definitions.ts +650 -0
  95. package/src/core/session-tracker.ts +6 -4
  96. package/src/core/shadow-observation-registry.ts +6 -3
  97. package/src/core/system-logger.ts +2 -2
  98. package/src/core/thinking-models.ts +182 -46
  99. package/src/core/thinking-os-parser.ts +156 -0
  100. package/src/core/training-program.ts +7 -7
  101. package/src/core/trajectory.ts +42 -36
  102. package/src/core/workspace-context.ts +77 -11
  103. package/src/core/workspace-dir-validation.ts +152 -0
  104. package/src/hooks/AGENTS.md +31 -0
  105. package/src/hooks/bash-risk.ts +3 -1
  106. package/src/hooks/edit-verification.ts +9 -5
  107. package/src/hooks/gate-block-helper.ts +5 -1
  108. package/src/hooks/gate.ts +152 -5
  109. package/src/hooks/gfi-gate.ts +9 -2
  110. package/src/hooks/lifecycle-routing.ts +124 -0
  111. package/src/hooks/lifecycle.ts +12 -12
  112. package/src/hooks/llm.ts +17 -109
  113. package/src/hooks/message-sanitize.ts +5 -3
  114. package/src/hooks/pain.ts +19 -15
  115. package/src/hooks/progressive-trust-gate.ts +7 -1
  116. package/src/hooks/prompt.ts +169 -60
  117. package/src/hooks/subagent.ts +5 -4
  118. package/src/hooks/thinking-checkpoint.ts +2 -0
  119. package/src/hooks/trajectory-collector.ts +15 -12
  120. package/src/http/principles-console-route.ts +31 -68
  121. package/src/i18n/commands.ts +2 -2
  122. package/src/index.ts +130 -40
  123. package/src/service/central-database.ts +131 -43
  124. package/src/service/central-health-service.ts +47 -0
  125. package/src/service/central-overview-service.ts +135 -0
  126. package/src/service/central-sync-service.ts +87 -0
  127. package/src/service/control-ui-query-service.ts +46 -36
  128. package/src/service/event-log-auditor.ts +261 -0
  129. package/src/service/evolution-query-service.ts +23 -22
  130. package/src/service/evolution-worker.ts +565 -261
  131. package/src/service/health-query-service.ts +213 -36
  132. package/src/service/nocturnal-runtime.ts +8 -4
  133. package/src/service/nocturnal-service.ts +503 -59
  134. package/src/service/nocturnal-target-selector.ts +5 -7
  135. package/src/service/runtime-summary-service.ts +2 -1
  136. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
  137. package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
  138. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
  139. package/src/service/subagent-workflow/index.ts +2 -0
  140. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
  141. package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
  142. package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
  143. package/src/service/subagent-workflow/types.ts +9 -4
  144. package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
  145. package/src/service/subagent-workflow/workflow-store.ts +71 -11
  146. package/src/service/trajectory-service.ts +2 -1
  147. package/src/tools/critique-prompt.ts +1 -1
  148. package/src/tools/deep-reflect.ts +175 -209
  149. package/src/tools/model-index.ts +2 -1
  150. package/src/types/event-types.ts +2 -2
  151. package/src/types/principle-tree-schema.ts +29 -23
  152. package/src/utils/file-lock.ts +5 -3
  153. package/src/utils/io.ts +5 -2
  154. package/src/utils/nlp.ts +5 -46
  155. package/src/utils/node-vm-polyfill.ts +11 -0
  156. package/src/utils/plugin-logger.ts +2 -0
  157. package/src/utils/retry.ts +572 -0
  158. package/src/utils/subagent-probe.ts +1 -1
  159. package/templates/langs/en/core/AGENTS.md +0 -13
  160. package/templates/langs/en/core/SOUL.md +1 -31
  161. package/templates/langs/en/core/TOOLS.md +0 -4
  162. package/templates/langs/en/principles/THINKING_OS.md +77 -0
  163. package/templates/langs/en/skills/admin/SKILL.md +0 -1
  164. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
  165. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
  166. package/templates/langs/zh/core/AGENTS.md +0 -22
  167. package/templates/langs/zh/core/SOUL.md +1 -31
  168. package/templates/langs/zh/core/TOOLS.md +0 -4
  169. package/templates/langs/zh/principles/THINKING_OS.md +77 -0
  170. package/templates/langs/zh/skills/admin/SKILL.md +0 -1
  171. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
  172. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
  173. package/tests/commands/evolution-status.test.ts +119 -0
  174. package/tests/commands/implementation-lifecycle.test.ts +362 -0
  175. package/tests/core/code-implementation-storage.test.ts +398 -0
  176. package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
  177. package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
  178. package/tests/core/nocturnal-artificer.test.ts +241 -0
  179. package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
  180. package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
  181. package/tests/core/pd-task-store.test.ts +126 -0
  182. package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
  183. package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
  184. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
  185. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
  186. package/tests/core/principle-training-state.test.ts +228 -1
  187. package/tests/core/principle-tree-ledger.test.ts +423 -0
  188. package/tests/core/regression-v1-9-1.test.ts +265 -0
  189. package/tests/core/replay-engine.test.ts +234 -0
  190. package/tests/core/rule-host-helpers.test.ts +120 -0
  191. package/tests/core/rule-host.test.ts +389 -0
  192. package/tests/core/rule-implementation-runtime.test.ts +64 -0
  193. package/tests/core/workspace-context.test.ts +53 -0
  194. package/tests/core/workspace-dir-validation.test.ts +272 -0
  195. package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
  196. package/tests/hooks/pain.test.ts +74 -10
  197. package/tests/hooks/prompt.test.ts +63 -1
  198. package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
  199. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
  200. package/tests/service/data-endpoints-regression.test.ts +834 -0
  201. package/tests/service/evolution-worker.test.ts +0 -123
  202. package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
  203. package/tests/utils/nlp.test.ts +1 -19
  204. package/tests/utils/retry.test.ts +327 -0
  205. package/ui/src/App.tsx +1 -1
  206. package/ui/src/api.ts +4 -0
  207. package/ui/src/charts.tsx +366 -0
  208. package/ui/src/components/WorkspaceConfig.tsx +107 -75
  209. package/ui/src/i18n/ui.ts +89 -31
  210. package/ui/src/pages/EvolutionPage.tsx +1 -1
  211. package/ui/src/pages/OverviewPage.tsx +441 -81
  212. package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
  213. package/ui/src/styles.css +43 -0
  214. package/ui/src/types.ts +17 -1
  215. package/src/agents/nocturnal-dreamer.md +0 -152
  216. package/src/agents/nocturnal-philosopher.md +0 -138
  217. package/src/agents/nocturnal-reflector.md +0 -126
  218. package/src/agents/nocturnal-scribe.md +0 -164
  219. package/templates/workspace/.principles/00-kernel.md +0 -51
  220. package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
  221. package/templates/workspace/.principles/PRINCIPLES.md +0 -20
  222. package/templates/workspace/.principles/PROFILE.json +0 -54
  223. package/templates/workspace/.principles/PROFILE.schema.json +0 -56
  224. package/templates/workspace/.principles/THINKING_OS.md +0 -64
  225. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
  226. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
  227. package/templates/workspace/.principles/models/_INDEX.md +0 -27
  228. package/templates/workspace/.principles/models/first_principles.md +0 -62
  229. package/templates/workspace/.principles/models/marketing_4p.md +0 -52
  230. package/templates/workspace/.principles/models/porter_five.md +0 -63
  231. package/templates/workspace/.principles/models/swot.md +0 -60
  232. package/templates/workspace/.principles/models/user_story_map.md +0 -63
  233. package/templates/workspace/.state/WORKBOARD.json +0 -4
@@ -22,7 +22,6 @@ import type {
22
22
  TaskKind,
23
23
  TaskPriority,
24
24
  EvolutionTaskInput,
25
- EvolutionTaskInputV2,
26
25
  EvolutionEventInput,
27
26
  EvolutionTaskRecord,
28
27
  EvolutionEventRecord,
@@ -50,7 +49,6 @@ export type {
50
49
  TaskKind,
51
50
  TaskPriority,
52
51
  EvolutionTaskInput,
53
- EvolutionTaskInputV2,
54
52
  EvolutionEventInput,
55
53
  EvolutionTaskRecord,
56
54
  EvolutionEventRecord,
@@ -210,7 +208,8 @@ export class TrajectoryDatabase {
210
208
  const createdAt = input.createdAt ?? nowIso();
211
209
  // Extract filePath from paramsJson if provided and is an object with filePath
212
210
  const paramsObj = input.paramsJson as Record<string, unknown> | undefined;
213
- const filePath = paramsObj && typeof paramsObj.filePath === 'string' ? paramsObj.filePath : null;
211
+ /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: _filePath extracted for potential future use but currently unused */
212
+ const _filePath = paramsObj && typeof paramsObj.filePath === 'string' ? paramsObj.filePath : null;
214
213
  const rowId = this.withWrite(() => {
215
214
  const result = this.db.prepare(`
216
215
  INSERT INTO tool_calls (
@@ -272,7 +271,7 @@ export class TrajectoryDatabase {
272
271
  * Search pain_events using FTS5 full-text search (MEM-04).
273
272
  * Returns pain events matching the query, ordered by relevance.
274
273
  */
275
- searchPainEvents(query: string, limit: number = 10): Array<{
274
+ searchPainEvents(query: string, limit = 10): {
276
275
  id: number;
277
276
  sessionId: string;
278
277
  source: string;
@@ -283,7 +282,7 @@ export class TrajectoryDatabase {
283
282
  confidence: number | null;
284
283
  text: string | null;
285
284
  createdAt: string;
286
- }> {
285
+ }[] {
287
286
  if (!query || query.trim().length === 0) {
288
287
  return [];
289
288
  }
@@ -299,7 +298,7 @@ export class TrajectoryDatabase {
299
298
  WHERE pain_events_fts MATCH ?
300
299
  ORDER BY bm25(pain_events_fts) DESC
301
300
  LIMIT ?
302
- `).all(ftsQuery, limit) as Array<{
301
+ `).all(ftsQuery, limit) as {
303
302
  id: number;
304
303
  session_id: string;
305
304
  source: string;
@@ -310,7 +309,7 @@ export class TrajectoryDatabase {
310
309
  confidence: number | null;
311
310
  text: string | null;
312
311
  created_at: string;
313
- }>;
312
+ }[];
314
313
 
315
314
  return results.map(row => ({
316
315
  id: row.id,
@@ -396,7 +395,7 @@ export class TrajectoryDatabase {
396
395
  recordEvolutionTask(input: EvolutionTaskInput): void {
397
396
  const now = nowIso();
398
397
  // Cast to V2 to access new fields
399
- const v2 = input as EvolutionTaskInputV2;
398
+ const v2 = input;
400
399
  this.withWrite(() => {
401
400
  this.db.prepare(`
402
401
  INSERT INTO evolution_tasks (
@@ -442,7 +441,7 @@ export class TrajectoryDatabase {
442
441
  updateEvolutionTask(taskId: string, updates: Partial<Omit<EvolutionTaskInput, 'taskId' | 'traceId' | 'source'>>): void {
443
442
  const now = nowIso();
444
443
  // Cast to V2 to access new fields
445
- const v2Updates = updates as Partial<Omit<EvolutionTaskInputV2, 'taskId' | 'traceId' | 'source'>>;
444
+ const v2Updates = updates;
446
445
  this.withWrite(() => {
447
446
  const setClauses: string[] = ['updated_at = ?'];
448
447
  const values: unknown[] = [now];
@@ -553,7 +552,7 @@ export class TrajectoryDatabase {
553
552
  ${whereClause}
554
553
  ORDER BY created_at DESC
555
554
  LIMIT ? OFFSET ?
556
- `).all(...values, limit, offset) as Array<Record<string, unknown>>;
555
+ `).all(...values, limit, offset) as Record<string, unknown>[];
557
556
 
558
557
  return rows.map((row) => ({
559
558
  id: Number(row.id),
@@ -588,7 +587,8 @@ export class TrajectoryDatabase {
588
587
  const limit = filters.limit ?? 100;
589
588
  const offset = filters.offset ?? 0;
590
589
 
591
- let rows: Array<Record<string, unknown>>;
590
+ // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
591
+ let rows: Record<string, unknown>[];
592
592
  if (traceId) {
593
593
  rows = this.db.prepare(`
594
594
  SELECT id, trace_id, task_id, stage, level, message, summary, metadata_json, created_at
@@ -596,14 +596,14 @@ export class TrajectoryDatabase {
596
596
  WHERE trace_id = ?
597
597
  ORDER BY created_at ASC
598
598
  LIMIT ? OFFSET ?
599
- `).all(traceId, limit, offset) as Array<Record<string, unknown>>;
599
+ `).all(traceId, limit, offset) as Record<string, unknown>[];
600
600
  } else {
601
601
  rows = this.db.prepare(`
602
602
  SELECT id, trace_id, task_id, stage, level, message, summary, metadata_json, created_at
603
603
  FROM evolution_events
604
604
  ORDER BY created_at DESC
605
605
  LIMIT ? OFFSET ?
606
- `).all(limit, offset) as Array<Record<string, unknown>>;
606
+ `).all(limit, offset) as Record<string, unknown>[];
607
607
  }
608
608
 
609
609
  return rows.map((row) => ({
@@ -669,7 +669,7 @@ export class TrajectoryDatabase {
669
669
  getEvolutionStats(): { total: number; pending: number; inProgress: number; completed: number; failed: number } {
670
670
  const rows = this.db.prepare(`
671
671
  SELECT status, COUNT(*) as count FROM evolution_tasks GROUP BY status
672
- `).all() as Array<{ status: string; count: number }>;
672
+ `).all() as { status: string; count: number }[];
673
673
 
674
674
  const stats = { total: 0, pending: 0, inProgress: 0, completed: 0, failed: 0 };
675
675
  for (const row of rows) {
@@ -691,7 +691,7 @@ export class TrajectoryDatabase {
691
691
  * @param options.dateFrom - Only return sessions updated after this date
692
692
  * @param options.dateTo - Only return sessions updated before this date
693
693
  */
694
- listRecentSessions(options: { limit?: number; dateFrom?: string; dateTo?: string } = {}): Array<{ sessionId: string; startedAt: string; updatedAt: string }> {
694
+ listRecentSessions(options: { limit?: number; dateFrom?: string; dateTo?: string } = {}): { sessionId: string; startedAt: string; updatedAt: string }[] {
695
695
  const conditions: string[] = [];
696
696
  const values: unknown[] = [];
697
697
 
@@ -713,7 +713,7 @@ export class TrajectoryDatabase {
713
713
  ${whereClause}
714
714
  ORDER BY updated_at DESC
715
715
  LIMIT ?
716
- `).all(...values, limit) as Array<Record<string, unknown>>;
716
+ `).all(...values, limit) as Record<string, unknown>[];
717
717
 
718
718
  return rows.map((row) => ({
719
719
  sessionId: String(row.session_id),
@@ -734,7 +734,7 @@ export class TrajectoryDatabase {
734
734
  FROM assistant_turns
735
735
  WHERE session_id = ?
736
736
  ORDER BY id ASC
737
- `).all(sessionId) as Array<Record<string, unknown>>;
737
+ `).all(sessionId) as Record<string, unknown>[];
738
738
 
739
739
  return rows.map((row) => ({
740
740
  id: Number(row.id),
@@ -755,7 +755,7 @@ export class TrajectoryDatabase {
755
755
  * Returns: Analytics data aggregated from trajectory database.
756
756
  * Not: Runtime truth or real-time queue state.
757
757
  */
758
- listToolCallsForSession(sessionId: string): Array<{
758
+ listToolCallsForSession(sessionId: string): {
759
759
  id: number;
760
760
  toolName: string;
761
761
  outcome: string;
@@ -767,14 +767,14 @@ export class TrajectoryDatabase {
767
767
  gfiBefore: number | null;
768
768
  gfiAfter: number | null;
769
769
  createdAt: string;
770
- }> {
770
+ }[] {
771
771
  const rows = this.db.prepare(`
772
772
  SELECT id, tool_name, outcome, params_json, duration_ms, exit_code, error_type, error_message,
773
773
  gfi_before, gfi_after, created_at
774
774
  FROM tool_calls
775
775
  WHERE session_id = ?
776
776
  ORDER BY id ASC
777
- `).all(sessionId) as Array<Record<string, unknown>>;
777
+ `).all(sessionId) as Record<string, unknown>[];
778
778
 
779
779
  return rows.map((row) => {
780
780
  // Extract filePath from params_json if present
@@ -783,6 +783,7 @@ export class TrajectoryDatabase {
783
783
  try {
784
784
  const params = JSON.parse(row.params_json);
785
785
  if (params && typeof params.filePath === 'string') {
786
+ // eslint-disable-next-line @typescript-eslint/prefer-destructuring -- Reason: filePath is a reassignable outer let variable - destructuring would lose the assignment semantics
786
787
  filePath = params.filePath;
787
788
  }
788
789
  } catch {
@@ -811,7 +812,7 @@ export class TrajectoryDatabase {
811
812
  * Returns: Analytics data aggregated from trajectory database.
812
813
  * Not: Runtime truth or real-time queue state.
813
814
  */
814
- listPainEventsForSession(sessionId: string): Array<{
815
+ listPainEventsForSession(sessionId: string): {
815
816
  id: number;
816
817
  source: string;
817
818
  score: number;
@@ -820,13 +821,13 @@ export class TrajectoryDatabase {
820
821
  origin: string | null;
821
822
  confidence: number | null;
822
823
  createdAt: string;
823
- }> {
824
+ }[] {
824
825
  const rows = this.db.prepare(`
825
826
  SELECT id, source, score, reason, severity, origin, confidence, created_at
826
827
  FROM pain_events
827
828
  WHERE session_id = ?
828
829
  ORDER BY created_at ASC
829
- `).all(sessionId) as Array<Record<string, unknown>>;
830
+ `).all(sessionId) as Record<string, unknown>[];
830
831
 
831
832
  return rows.map((row) => ({
832
833
  id: Number(row.id),
@@ -844,19 +845,19 @@ export class TrajectoryDatabase {
844
845
  * List user turns for a session.
845
846
  * Returns sanitized/reduced fields for nocturnal use — NO raw text.
846
847
  */
847
- listUserTurnsForSession(sessionId: string): Array<{
848
+ listUserTurnsForSession(sessionId: string): {
848
849
  id: number;
849
850
  turnIndex: number;
850
851
  correctionDetected: boolean;
851
852
  correctionCue: string | null;
852
853
  createdAt: string;
853
- }> {
854
+ }[] {
854
855
  const rows = this.db.prepare(`
855
856
  SELECT id, turn_index, correction_detected, correction_cue, created_at
856
857
  FROM user_turns
857
858
  WHERE session_id = ?
858
859
  ORDER BY turn_index ASC
859
- `).all(sessionId) as Array<Record<string, unknown>>;
860
+ `).all(sessionId) as Record<string, unknown>[];
860
861
 
861
862
  return rows.map((row) => ({
862
863
  id: Number(row.id),
@@ -881,7 +882,7 @@ export class TrajectoryDatabase {
881
882
  FROM correction_samples
882
883
  WHERE review_status = ?
883
884
  ORDER BY created_at DESC
884
- `).all(status) as Array<Record<string, unknown>>;
885
+ `).all(status) as Record<string, unknown>[];
885
886
 
886
887
  return rows.map((row) => ({
887
888
  sampleId: String(row.sample_id),
@@ -903,20 +904,20 @@ export class TrajectoryDatabase {
903
904
  * List gate blocks for a session.
904
905
  * Returns minimal fields for nocturnal use — no raw text.
905
906
  */
906
- listGateBlocksForSession(sessionId: string): Array<{
907
+ listGateBlocksForSession(sessionId: string): {
907
908
  id: number;
908
909
  toolName: string;
909
910
  filePath: string | null;
910
911
  reason: string;
911
912
  planStatus: string | null;
912
913
  createdAt: string;
913
- }> {
914
+ }[] {
914
915
  const rows = this.db.prepare(`
915
916
  SELECT id, tool_name, file_path, reason, plan_status, created_at
916
917
  FROM gate_blocks
917
918
  WHERE session_id = ?
918
919
  ORDER BY id ASC
919
- `).all(sessionId) as Array<Record<string, unknown>>;
920
+ `).all(sessionId) as Record<string, unknown>[];
920
921
 
921
922
  return rows.map((row) => ({
922
923
  id: Number(row.id),
@@ -992,7 +993,7 @@ export class TrajectoryDatabase {
992
993
  JOIN user_turns ut ON ut.id = cs.user_correction_turn_id
993
994
  WHERE (? = 0 OR cs.review_status = 'approved')
994
995
  ORDER BY cs.created_at ASC
995
- `).all(opts.approvedOnly ? 1 : 0) as Array<Record<string, unknown>>;
996
+ `).all(opts.approvedOnly ? 1 : 0) as Record<string, unknown>[];
996
997
 
997
998
  const exportPath = path.join(this.exportDir, `corrections-${Date.now()}-${opts.mode}.jsonl`);
998
999
  const lines = rows.map((row) => {
@@ -1271,7 +1272,7 @@ export class TrajectoryDatabase {
1271
1272
  ];
1272
1273
  for (const col of v2Columns) {
1273
1274
  const exists = this.db.prepare(`PRAGMA table_info(evolution_tasks)`).all()
1274
- .some((row: any) => row.name === col.name);
1275
+ .some((row) => (row as Record<string, unknown>).name === col.name);
1275
1276
  if (!exists) {
1276
1277
  this.db.exec(`ALTER TABLE evolution_tasks ADD COLUMN ${col.name} ${col.type}`);
1277
1278
  }
@@ -1323,6 +1324,7 @@ export class TrajectoryDatabase {
1323
1324
  this.importLegacyEvolution();
1324
1325
  }
1325
1326
 
1327
+ /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: _fromVersion reserved for future migration logic */
1326
1328
  private migrateSchema(_fromVersion?: number): void {
1327
1329
  this.db.exec(`
1328
1330
  DROP VIEW IF EXISTS v_daily_metrics;
@@ -1509,7 +1511,7 @@ export class TrajectoryDatabase {
1509
1511
  WHERE session_id = ? AND outcome = 'success'
1510
1512
  ORDER BY id DESC
1511
1513
  LIMIT 3
1512
- `).all(sessionId) as Array<Record<string, unknown>>;
1514
+ `).all(sessionId) as Record<string, unknown>[];
1513
1515
  if (successfulCalls.length === 0) return;
1514
1516
 
1515
1517
  const sampleId = `sample_${crypto.createHash('md5').update(`${sessionId}:${correctionTurn.id}:${successfulCalls[0].id}`).digest('hex').slice(0, 12)}`;
@@ -1543,6 +1545,7 @@ export class TrajectoryDatabase {
1543
1545
  });
1544
1546
  }
1545
1547
 
1548
+ /* eslint-disable @typescript-eslint/max-params -- Reason: Audit record requires exportKind, mode, approvedOnly, filePath, and rowCount */
1546
1549
  private recordExportAudit(
1547
1550
  exportKind: string,
1548
1551
  mode: CorrectionExportMode,
@@ -1595,7 +1598,7 @@ export class TrajectoryDatabase {
1595
1598
  SELECT blob_ref FROM assistant_turns WHERE blob_ref IS NOT NULL
1596
1599
  UNION
1597
1600
  SELECT blob_ref FROM user_turns WHERE blob_ref IS NOT NULL
1598
- `).all() as Array<{ blob_ref?: string | null }>;
1601
+ `).all() as { blob_ref?: string | null }[];
1599
1602
  for (const row of rows) {
1600
1603
  if (row.blob_ref) referenced.add(String(row.blob_ref));
1601
1604
  }
@@ -1607,6 +1610,7 @@ export class TrajectoryDatabase {
1607
1610
  for (const entry of fs.readdirSync(this.blobDir)) {
1608
1611
  if (referenced.has(entry)) continue;
1609
1612
  const fullPath = path.join(this.blobDir, entry);
1613
+ // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch continues
1610
1614
  let stat: fs.Stats;
1611
1615
  try {
1612
1616
  stat = fs.statSync(fullPath);
@@ -1629,7 +1633,7 @@ export class TrajectoryDatabase {
1629
1633
  }
1630
1634
 
1631
1635
  export class TrajectoryRegistry {
1632
- private static instances = new Map<string, TrajectoryDatabase>();
1636
+ private static readonly instances = new Map<string, TrajectoryDatabase>();
1633
1637
 
1634
1638
  static get(workspaceDir: string, opts: Omit<TrajectoryDatabaseOptions, 'workspaceDir'> = {}): TrajectoryDatabase {
1635
1639
  const normalized = path.resolve(workspaceDir);
@@ -1656,7 +1660,9 @@ export class TrajectoryRegistry {
1656
1660
  this.instances.clear();
1657
1661
  }
1658
1662
 
1659
- static use<T>(workspaceDir: string, fn: (db: TrajectoryDatabase) => T, opts: Omit<TrajectoryDatabaseOptions, 'workspaceDir'> = {}): T {
1663
+ /* eslint-disable no-unused-vars -- Reason: db parameter name in callback type signature */
1664
+ static use<T>(workspaceDir: string, fn: (_db: TrajectoryDatabase) => T, opts: Omit<TrajectoryDatabaseOptions, 'workspaceDir'> = {}): T {
1665
+ /* eslint-enable no-unused-vars */
1660
1666
  const normalized = path.resolve(workspaceDir);
1661
1667
  const existing = this.instances.get(normalized);
1662
1668
  if (existing) {
@@ -1,21 +1,41 @@
1
- import { resolvePdPath, PD_FILES } from './paths.js';
1
+ import type { PD_FILES } from './paths.js';
2
+ import { resolvePdPath } from './paths.js';
2
3
  import { PathResolver } from './path-resolver.js';
3
4
  import { ConfigService } from './config-service.js';
4
- import { PainConfig } from './config.js';
5
- import { EventLogService, EventLog } from './event-log.js';
5
+ import type { PainConfig } from './config.js';
6
+ import type { EventLog } from './event-log.js';
7
+ import { EventLogService } from './event-log.js';
6
8
  import { DictionaryService } from './dictionary-service.js';
7
- import { PainDictionary } from './dictionary.js';
9
+ import type { PainDictionary } from './dictionary.js';
8
10
  import { HygieneTracker } from './hygiene/tracker.js';
9
11
  import { EvolutionReducerImpl } from './evolution-reducer.js';
10
- import { TrajectoryDatabase, TrajectoryRegistry, TrajectoryDatabaseOptions } from './trajectory.js';
12
+ import type { TrajectoryDatabase, TrajectoryDatabaseOptions } from './trajectory.js';
13
+ import { TrajectoryRegistry } from './trajectory.js';
14
+ import { PrincipleLifecycleService } from './principle-internalization/principle-lifecycle-service.js';
15
+ import {
16
+ getPrincipleSubtree,
17
+ updatePrinciple,
18
+ updatePrincipleValueMetrics,
19
+ type PrincipleSubtree,
20
+ } from './principle-tree-ledger.js';
21
+ import type { Principle, PrincipleValueMetrics } from '../types/principle-tree-schema.js';
22
+ import type { Principle as ActivePrinciple } from './evolution-types.js';
23
+
24
+ /* eslint-disable no-unused-vars -- Reason: interface method param names intentionally unused - implementations provide actual names */
25
+ interface PrincipleTreeLedgerAccessor {
26
+ getPrincipleSubtree(_principleId: string): PrincipleSubtree | undefined;
27
+ updatePrinciple(_principleId: string, updates: Partial<Principle>): Principle;
28
+ updatePrincipleValueMetrics(principleId: string, _metrics: PrincipleValueMetrics): PrincipleValueMetrics;
29
+ }
30
+ /* eslint-enable no-unused-vars */
11
31
 
12
32
  /**
13
33
  * WorkspaceContext - Centralized management of workspace-specific paths and services.
14
34
  * Implements a cached singleton pattern per workspace directory.
15
35
  */
16
36
  export class WorkspaceContext {
17
- private static instances = new Map<string, WorkspaceContext>();
18
- private static pathResolver = new PathResolver();
37
+ private static readonly instances = new Map<string, WorkspaceContext>();
38
+ private static readonly pathResolver = new PathResolver();
19
39
 
20
40
  public readonly workspaceDir: string;
21
41
  public readonly stateDir: string;
@@ -26,6 +46,8 @@ export class WorkspaceContext {
26
46
  private _hygiene?: HygieneTracker;
27
47
  private _evolutionReducer?: EvolutionReducerImpl;
28
48
  private _trajectory?: TrajectoryDatabase;
49
+ private _principleTreeLedger?: PrincipleTreeLedgerAccessor;
50
+ private _principleLifecycle?: PrincipleLifecycleService;
29
51
 
30
52
  private constructor(workspaceDir: string, stateDir: string) {
31
53
  this.workspaceDir = workspaceDir;
@@ -78,7 +100,7 @@ export class WorkspaceContext {
78
100
  */
79
101
  get evolutionReducer(): EvolutionReducerImpl {
80
102
  if (!this._evolutionReducer) {
81
- this._evolutionReducer = new EvolutionReducerImpl({ workspaceDir: this.workspaceDir });
103
+ this._evolutionReducer = new EvolutionReducerImpl({ workspaceDir: this.workspaceDir, stateDir: this.stateDir });
82
104
  }
83
105
  return this._evolutionReducer;
84
106
  }
@@ -93,6 +115,47 @@ export class WorkspaceContext {
93
115
  return this._trajectory;
94
116
  }
95
117
 
118
+ /**
119
+ * Locked ledger access for principle tree reads and metric writes in this workspace.
120
+ */
121
+ get principleTreeLedger(): PrincipleTreeLedgerAccessor {
122
+ if (!this._principleTreeLedger) {
123
+ this._principleTreeLedger = {
124
+ getPrincipleSubtree: (principleId: string) => getPrincipleSubtree(this.stateDir, principleId),
125
+ updatePrinciple: (principleId: string, updates: Partial<Principle>) =>
126
+ updatePrinciple(this.stateDir, principleId, updates),
127
+ updatePrincipleValueMetrics: (principleId: string, metrics: PrincipleValueMetrics) =>
128
+ updatePrincipleValueMetrics(this.stateDir, principleId, metrics),
129
+ };
130
+ }
131
+ return this._principleTreeLedger;
132
+ }
133
+
134
+ /**
135
+ * Phase 15 lifecycle/read-model surface for metrics, assessments, and route recommendations.
136
+ */
137
+ get principleLifecycle(): PrincipleLifecycleService {
138
+ if (!this._principleLifecycle) {
139
+ this._principleLifecycle = new PrincipleLifecycleService(this.workspaceDir, this.stateDir);
140
+ }
141
+ return this._principleLifecycle;
142
+ }
143
+
144
+ /**
145
+ * Retrieve active Principle -> Rule -> Implementation subtrees without bypassing reducer authority.
146
+ */
147
+ getActivePrincipleSubtrees(): { principle: ActivePrinciple; subtree: PrincipleSubtree }[] {
148
+ return this.evolutionReducer
149
+ .getActivePrinciples()
150
+ .map((principle) => {
151
+ const subtree = this.principleTreeLedger.getPrincipleSubtree(principle.id);
152
+ return subtree ? { principle, subtree } : null;
153
+ })
154
+ .filter(
155
+ (entry): entry is { principle: ActivePrinciple; subtree: PrincipleSubtree } => entry !== null,
156
+ );
157
+ }
158
+
96
159
  private getTrajectoryOptions(): Omit<TrajectoryDatabaseOptions, 'workspaceDir'> {
97
160
  const inlineThreshold = Number(this.config.get('trajectory.blob_inline_threshold_bytes'));
98
161
  const busyTimeoutMs = Number(this.config.get('trajectory.busy_timeout_ms'));
@@ -110,12 +173,13 @@ export class WorkspaceContext {
110
173
  * Uses PathResolver to handle path normalization and fallback logic.
111
174
  * @throws Error if workspaceDir is missing and no fallback available.
112
175
  */
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: OpenClaw plugin framework hook context has dynamic shape - type not available
113
177
  static fromHookContext(ctx: any): WorkspaceContext {
114
- const logger = ctx.logger;
178
+ const {logger} = ctx;
115
179
  const log = (msg: string) => logger?.info?.(msg);
116
180
  const logWarn = (msg: string) => logger?.warn?.(msg);
117
181
 
118
- let workspaceDir = ctx.workspaceDir;
182
+ let {workspaceDir} = ctx;
119
183
 
120
184
  if (!workspaceDir) {
121
185
  logWarn('[PD:WorkspaceContext] workspaceDir not provided in context, using PathResolver fallback');
@@ -132,7 +196,7 @@ export class WorkspaceContext {
132
196
  const existing = this.instances.get(workspaceDir);
133
197
  if (existing) return existing;
134
198
 
135
- let stateDir = ctx.stateDir;
199
+ let {stateDir} = ctx;
136
200
  if (!stateDir) {
137
201
  stateDir = resolvePdPath(workspaceDir, 'STATE_DIR');
138
202
  log(`[PD:WorkspaceContext] Computed stateDir: ${stateDir}`);
@@ -162,6 +226,8 @@ export class WorkspaceContext {
162
226
  this._dictionary = undefined;
163
227
  this._evolutionReducer = undefined;
164
228
  this._trajectory = undefined;
229
+ this._principleTreeLedger = undefined;
230
+ this._principleLifecycle = undefined;
165
231
  }
166
232
 
167
233
  /**
@@ -0,0 +1,152 @@
1
+ /**
2
+ * WorkspaceDir Validation Utilities
3
+ *
4
+ * Provides runtime validation of workspaceDir to catch OpenClaw context bugs early.
5
+ * When a hook receives an invalid workspaceDir, we warn immediately rather than
6
+ * silently writing to the wrong directory.
7
+ */
8
+
9
+ /* eslint-disable no-unused-vars -- Reason: type definitions require param names that implementations may not use */
10
+
11
+ import * as os from 'os';
12
+ import type { PluginLogger } from '../openclaw-sdk.js';
13
+
14
+ /**
15
+ * Check if a path looks like a home directory (not a real workspace).
16
+ * Returns the reason if suspicious, or null if it looks valid.
17
+ */
18
+ export function validateWorkspaceDir(dir: string | undefined): string | null {
19
+ if (!dir) {
20
+ return 'workspaceDir is undefined/null';
21
+ }
22
+
23
+ const homeDir = os.homedir();
24
+
25
+ // Home directory itself is not a valid workspace
26
+ if (dir === homeDir) {
27
+ return `workspaceDir equals home directory (${homeDir}), likely missing context field`;
28
+ }
29
+
30
+ // Root directory is definitely not a workspace
31
+ if (dir === '/' || dir === '') {
32
+ return `workspaceDir is root or empty: "${dir}"`;
33
+ }
34
+
35
+ // Check if it looks like a resolved '.' that went wrong
36
+ // Common bad patterns:
37
+ const badPatterns = [
38
+ // Directly under home without a workspace subdirectory
39
+ { pattern: new RegExp(`^${homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`), desc: 'is home directory itself' },
40
+ { pattern: new RegExp(`^${homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/$`), desc: 'is home directory with trailing slash' },
41
+ ];
42
+
43
+ for (const { pattern, desc } of badPatterns) {
44
+ if (pattern.test(dir)) {
45
+ return `workspaceDir ${desc}: "${dir}"`;
46
+ }
47
+ }
48
+
49
+ return null; // Looks valid
50
+ }
51
+
52
+ /**
53
+ * Try to resolve workspaceDir from agentId.
54
+ * Returns the resolved path if successful and valid, or null.
55
+ */
56
+ function tryResolveFromAgentId(
57
+ agentId: string,
58
+ api: {
59
+ runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
60
+ config: unknown;
61
+ },
62
+ onWarning: (msg: string) => void,
63
+ ): string | null {
64
+ try {
65
+ const resolved = api.runtime.agent.resolveAgentWorkspaceDir(api.config, agentId);
66
+ const issue = validateWorkspaceDir(resolved);
67
+ if (issue) {
68
+ onWarning(`agentId resolution returned invalid: "${resolved}" (${issue})`);
69
+ return null;
70
+ }
71
+ return resolved;
72
+ } catch (err) {
73
+ onWarning(`failed to resolve from agentId: ${String(err)}`);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Validate a fallback workspaceDir and warn if invalid.
80
+ * Returns the path regardless (it's the last resort).
81
+ */
82
+ function validateFallback(path: string, onWarning: (msg: string) => void): string {
83
+ const issue = validateWorkspaceDir(path);
84
+ if (issue) {
85
+ onWarning(`FINAL FALLBACK "${path}" is also invalid: ${issue}. Events will be written to wrong location!`);
86
+ }
87
+ return path;
88
+ }
89
+
90
+ /**
91
+ * Resolve workspaceDir with validation and warning.
92
+ *
93
+ * Usage:
94
+ * const workspaceDir = resolveValidWorkspaceDir(ctx, api, { source: 'after_tool_call' });
95
+ *
96
+ * Fallback chain:
97
+ * 1. ctx.workspaceDir (validated)
98
+ * 2. api.runtime.agent.resolveAgentWorkspaceDir(config, ctx.agentId)
99
+ * 3. api.resolvePath('.') (last resort, warns loudly)
100
+ */
101
+ export function resolveValidWorkspaceDir(
102
+ ctx: { workspaceDir?: string; agentId?: string },
103
+ api: {
104
+ runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
105
+ config: unknown;
106
+ resolvePath: (input: string) => string;
107
+ logger: PluginLogger;
108
+ },
109
+ options?: { source?: string; onWarning?: (msg: string) => void },
110
+ ): string {
111
+ const source = options?.source || 'unknown';
112
+ const onWarning = options?.onWarning || ((msg: string) => api.logger.warn(`[PD:workspaceDir] ${msg}`));
113
+
114
+ // 1. Try ctx.workspaceDir
115
+ if (ctx.workspaceDir) {
116
+ const issue = validateWorkspaceDir(ctx.workspaceDir);
117
+ if (issue) {
118
+ onWarning(`${source}: ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
119
+ } else {
120
+ return ctx.workspaceDir;
121
+ }
122
+ }
123
+
124
+ // 2. Try agentId resolution
125
+ if (ctx.agentId) {
126
+ const fromAgent = tryResolveFromAgentId(ctx.agentId, api, onWarning);
127
+ if (fromAgent) return fromAgent;
128
+ }
129
+
130
+ // 3. Final fallback
131
+ return validateFallback(api.resolvePath('.'), onWarning);
132
+ }
133
+
134
+ /**
135
+ * Log workspaceDir resolution for debugging.
136
+ * Call this once during plugin startup to verify hook contexts.
137
+ */
138
+ export function logWorkspaceDirHealth(ctx: { workspaceDir?: string; agentId?: string }, source: string, api: {
139
+ runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
140
+ config: unknown;
141
+ resolvePath: (input: string) => string;
142
+ logger: PluginLogger;
143
+ }): void {
144
+ const resolved = resolveValidWorkspaceDir(ctx, api, { source });
145
+ const issue = validateWorkspaceDir(resolved);
146
+
147
+ if (issue) {
148
+ api.logger.error(`[PD:health] ${source}: workspaceDir="${resolved}" - ${issue}`);
149
+ } else {
150
+ api.logger.info(`[PD:health] ${source}: workspaceDir="${resolved}" ✓`);
151
+ }
152
+ }
@@ -0,0 +1,31 @@
1
+ # src/hooks/ — OpenClaw Lifecycle Hooks
2
+
3
+ **8 TypeScript files.** Intercepts agent behavior at key moments: prompt building, tool calls, session lifecycle.
4
+
5
+ ## WHERE TO LOOK
6
+
7
+ | Hook Event | File | What It Does |
8
+ |------------|------|--------------|
9
+ | `before_prompt_build` | `prompt.ts` | Multi-layer context injection: identity, trust, evolution, principles, thinking OS |
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 |
12
+ | `before_compaction` | `lifecycle.ts` | Checkpoints state before context loss |
13
+ | `after_compaction` | `lifecycle.ts` | State recovery |
14
+ | `before_reset` / `session_*` | `lifecycle.ts` | Session lifecycle management |
15
+ | `llm_output` | `llm.ts` | Analyzes LLM responses for pain signals |
16
+ | `subagent_*` | `subagent.ts` | Ensures sub-agents inherit mental models |
17
+ | `before_message_write` | `message-sanitize.ts` | Strips sensitive data from messages |
18
+ | — | `trajectory-collector.ts` | Collects tool call trajectories for SQLite analytics |
19
+
20
+ ## CONVENTIONS
21
+
22
+ - All hooks receive `HookContext` → use `WorkspaceContext.fromHookContext(ctx)` for services
23
+ - Gate hook is the most security-critical — handles trust stages 1-4, risk paths, bash security
24
+ - Pain hook feeds into EvolutionReducer event sourcing
25
+ - Prompt hook is the main injection point for agent context
26
+
27
+ ## ANTI-PATTERNS
28
+
29
+ - ❌ Gate hook must fail-closed (invalid regex → block, not allow)
30
+ - ❌ Never modify hook return values after they're computed
31
+ - ❌ Pain hook must not throw — errors are logged, not propagated