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
@@ -1,7 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import type { OpenClawPluginApi } from '../openclaw-sdk.js';
4
- import { PD_DIRS, PD_FILES, resolvePdPath } from './paths.js';
4
+ import type { PD_FILES} from './paths.js';
5
+ import { resolvePdPath } from './paths.js';
5
6
 
6
7
  /**
7
8
  * Handles migration of Principles Disciple files from legacy directories
@@ -13,11 +14,10 @@ export function migrateDirectoryStructure(api: OpenClawPluginApi, workspaceDir:
13
14
  const legacyStateDir = path.join(workspaceDir, 'memory', '.state');
14
15
 
15
16
  // Comprehensive migration map covering ALL legacy locations
16
- const migrationMap: Array<{ legacy: string; newKey: keyof typeof PD_FILES }> = [
17
+ const migrationMap: { legacy: string; newKey: keyof typeof PD_FILES }[] = [
17
18
  // From docs/
18
19
  { legacy: path.join(legacyDocsDir, 'PRINCIPLES.md'), newKey: 'PRINCIPLES' },
19
20
  { legacy: path.join(legacyDocsDir, 'THINKING_OS.md'), newKey: 'THINKING_OS' },
20
- { legacy: path.join(legacyDocsDir, '00-kernel.md'), newKey: 'KERNEL' },
21
21
  { legacy: path.join(legacyDocsDir, 'DECISION_POLICY.json'), newKey: 'DECISION_POLICY' },
22
22
  { legacy: path.join(legacyDocsDir, 'PLAN.md'), newKey: 'PLAN' },
23
23
  { legacy: path.join(legacyDocsDir, 'evolution_queue.json'), newKey: 'EVOLUTION_QUEUE' },
@@ -228,7 +228,7 @@ function readRegistry(stateDir: string): ModelDeploymentRegistry {
228
228
  return registry;
229
229
  } catch (err) {
230
230
  if (err instanceof SyntaxError || err instanceof Error) {
231
- throw new Error(`Failed to read deployment registry from "${registryPath}": ${err.message}`);
231
+ throw new Error(`Failed to read deployment registry from "${registryPath}": ${err.message}`, { cause: err });
232
232
  }
233
233
  throw err;
234
234
  }
@@ -249,9 +249,10 @@ function writeRegistry(stateDir: string, registry: ModelDeploymentRegistry): voi
249
249
  /**
250
250
  * Execute a read-modify-write under an exclusive file lock.
251
251
  */
252
+ /* eslint-disable no-unused-vars -- Reason: _registry is a type signature parameter */
252
253
  function withDeploymentRegistryLock<T>(
253
254
  stateDir: string,
254
- fn: (registry: ModelDeploymentRegistry) => T
255
+ fn: (_registry: ModelDeploymentRegistry) => T
255
256
  ): T {
256
257
  const registryPath = getRegistryPath(stateDir);
257
258
  return withLock(registryPath, () => {
@@ -348,6 +349,7 @@ export function assertPromotionGatePassed(stateDir: string, checkpointId: string
348
349
  * @throws Error if checkpoint is not found or not deployable
349
350
  * @throws Error if checkpoint's targetModelFamily violates profile constraints
350
351
  */
352
+ // eslint-disable-next-line @typescript-eslint/max-params -- Reason: checkpoint binding requires state + profile + checkpoint - refactoring would break API
351
353
  export function bindCheckpointToWorkerProfile(
352
354
  stateDir: string,
353
355
  workerProfile: WorkerProfile,
@@ -451,7 +453,7 @@ export function listDeployments(
451
453
  }
452
454
  ): Deployment[] {
453
455
  const registry = readRegistry(stateDir);
454
- let deployments = registry.deployments;
456
+ let {deployments} = registry;
455
457
 
456
458
  if (filter?.workerProfile) {
457
459
  deployments = deployments.filter((d) => d.workerProfile === filter.workerProfile);
@@ -711,7 +713,7 @@ export function getDeploymentRegistryStats(
711
713
  profilesWithRoutingEnabled: number;
712
714
  } {
713
715
  const registry = readRegistry(stateDir);
714
- const deployments = registry.deployments;
716
+ const {deployments} = registry;
715
717
 
716
718
  return {
717
719
  totalDeployments: deployments.length,
@@ -249,7 +249,8 @@ function writeRegistry(stateDir: string, registry: ModelTrainingRegistry): void
249
249
  */
250
250
  function withRegistryLock<T>(
251
251
  stateDir: string,
252
- fn: (registry: ModelTrainingRegistry) => T
252
+ // eslint-disable-next-line no-unused-vars -- parameter type annotation requires name
253
+ fn: (_: ModelTrainingRegistry) => T
253
254
  ): T {
254
255
  const registryPath = getRegistryPath(stateDir);
255
256
  return withLock(registryPath, () => {
@@ -320,6 +321,7 @@ export function registerTrainingRun(
320
321
  *
321
322
  * @throws Error if run not found or transition is invalid
322
323
  */
324
+ // eslint-disable-next-line @typescript-eslint/max-params -- Reason: status update requires state + runId + status - refactoring would break API
323
325
  export function updateTrainingRunStatus(
324
326
  stateDir: string,
325
327
  trainRunId: string,
@@ -498,7 +500,7 @@ export function listCheckpoints(
498
500
  }
499
501
  ): Checkpoint[] {
500
502
  const registry = readRegistry(stateDir);
501
- let checkpoints = registry.checkpoints;
503
+ let {checkpoints} = registry;
502
504
 
503
505
  if (filter?.trainRunId) {
504
506
  checkpoints = checkpoints.filter((c) => c.trainRunId === filter.trainRunId);
@@ -795,7 +797,7 @@ export function getTrainingRegistryStats(
795
797
  const registry = readRegistry(stateDir);
796
798
 
797
799
  const runs = registry.trainingRuns;
798
- const checkpoints = registry.checkpoints;
800
+ const {checkpoints} = registry;
799
801
  const evals = registry.evalSummaries;
800
802
 
801
803
  return {
@@ -22,8 +22,6 @@
22
22
  * - No LLM involvement — all checks are algorithmic
23
23
  */
24
24
 
25
- import type { NocturnalSessionSnapshot } from './nocturnal-trajectory-extractor.js';
26
-
27
25
  // ---------------------------------------------------------------------------
28
26
  // Types
29
27
  // ---------------------------------------------------------------------------
@@ -218,7 +216,7 @@ export function validateDreamerOutput(output: unknown): TrinityStageValidationRe
218
216
  });
219
217
 
220
218
  // Check for duplicate candidateIndices
221
- const indices = (obj.candidates as Array<Record<string, unknown>>)
219
+ const indices = (obj.candidates as Record<string, unknown>[])
222
220
  .map((c) => c.candidateIndex)
223
221
  .filter((i) => typeof i === 'number');
224
222
  const uniqueIndices = new Set(indices);
@@ -287,7 +285,7 @@ export function validatePhilosopherOutput(output: unknown): TrinityStageValidati
287
285
  });
288
286
 
289
287
  // Check ranks are unique and sequential (1, 2, 3...)
290
- const ranks = (obj.judgments as Array<Record<string, unknown>>)
288
+ const ranks = (obj.judgments as Record<string, unknown>[])
291
289
  .map((j) => j.rank)
292
290
  .filter((r) => typeof r === 'number')
293
291
  .sort((a, b) => a - b);
@@ -495,7 +493,7 @@ export function validateArtifact(
495
493
  }
496
494
 
497
495
  // Rule 3: Required string fields must be present and non-empty
498
- const requiredFields: Array<{ key: keyof RawReflectionArtifact; label: string }> = [
496
+ const requiredFields: { key: keyof RawReflectionArtifact; label: string }[] = [
499
497
  { key: 'artifactId', label: 'artifactId' },
500
498
  { key: 'sessionId', label: 'sessionId' },
501
499
  { key: 'principleId', label: 'principleId' },
@@ -522,7 +520,7 @@ export function validateArtifact(
522
520
 
523
521
  // Rule 4: Cross-validate principleId
524
522
  if (options.expectedPrincipleId !== undefined) {
525
- const principleId = obj.principleId;
523
+ const {principleId} = obj;
526
524
  if (!isNonEmptyString(principleId)) {
527
525
  failures.push({ reason: 'principleId is required but missing', field: 'principleId' });
528
526
  } else if (String(principleId) !== options.expectedPrincipleId) {
@@ -535,7 +533,7 @@ export function validateArtifact(
535
533
 
536
534
  // Rule 5: Cross-validate sessionId
537
535
  if (options.expectedSessionId !== undefined) {
538
- const sessionId = obj.sessionId;
536
+ const {sessionId} = obj;
539
537
  if (!isNonEmptyString(sessionId)) {
540
538
  failures.push({ reason: 'sessionId is required but missing', field: 'sessionId' });
541
539
  } else if (String(sessionId) !== options.expectedSessionId) {
@@ -547,7 +545,7 @@ export function validateArtifact(
547
545
  }
548
546
 
549
547
  // Rule 6: Check for placeholder values in text fields
550
- const textFields: Array<keyof RawReflectionArtifact> = [
548
+ const textFields: (keyof RawReflectionArtifact)[] = [
551
549
  'badDecision',
552
550
  'betterDecision',
553
551
  'rationale',
@@ -569,14 +567,14 @@ export function validateArtifact(
569
567
  }
570
568
 
571
569
  // Rule 7: Check sourceSnapshotRef if present
572
- const sourceSnapshotRef = obj.sourceSnapshotRef;
570
+ const {sourceSnapshotRef} = obj;
573
571
  if (sourceSnapshotRef !== undefined && !isNonEmptyString(sourceSnapshotRef)) {
574
572
  failures.push({ reason: 'Field "sourceSnapshotRef" must be a non-empty string if present', field: 'sourceSnapshotRef' });
575
573
  }
576
574
 
577
575
  // Rule 8: badDecision should not be identical to betterDecision
578
- const badDecision = obj.badDecision;
579
- const betterDecision = obj.betterDecision;
576
+ const {badDecision} = obj;
577
+ const {betterDecision} = obj;
580
578
  if (isNonEmptyString(badDecision) && isNonEmptyString(betterDecision)) {
581
579
  if (String(badDecision).trim() === String(betterDecision).trim()) {
582
580
  failures.push({
@@ -587,7 +585,7 @@ export function validateArtifact(
587
585
  }
588
586
 
589
587
  // Rule 9: Rationale should not be too short (needs explanation)
590
- const rationale = obj.rationale;
588
+ const {rationale} = obj;
591
589
  if (isNonEmptyString(rationale) && String(rationale).trim().length < 20) {
592
590
  failures.push({
593
591
  reason: 'rationale is too short — must provide meaningful explanation',
@@ -596,7 +594,7 @@ export function validateArtifact(
596
594
  }
597
595
 
598
596
  // Rule 10: Validate optional reflection quality metrics (if present)
599
- const thinkingModelDelta = obj.thinkingModelDelta;
597
+ const {thinkingModelDelta} = obj;
600
598
  if (thinkingModelDelta !== undefined && typeof thinkingModelDelta !== 'number') {
601
599
  failures.push({
602
600
  reason: 'thinkingModelDelta must be a number if present',
@@ -609,7 +607,7 @@ export function validateArtifact(
609
607
  });
610
608
  }
611
609
 
612
- const planningRatioGain = obj.planningRatioGain;
610
+ const {planningRatioGain} = obj;
613
611
  if (planningRatioGain !== undefined && typeof planningRatioGain !== 'number') {
614
612
  failures.push({
615
613
  reason: 'planningRatioGain must be a number if present',
@@ -691,6 +689,7 @@ export function parseAndValidateArtifact(
691
689
  options: ArbiterOptions = {}
692
690
  ): ArbiterResult {
693
691
  // Step 1: Parse JSON
692
+ // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned in try block immediately after declaration
694
693
  let parsed: unknown;
695
694
  try {
696
695
  parsed = JSON.parse(jsonString);
@@ -0,0 +1,117 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { resolveNocturnalDir } from './nocturnal-paths.js';
4
+ import { withLock } from '../utils/file-lock.js';
5
+
6
+ export type ArtifactKind = 'behavioral-sample' | 'rule-implementation-candidate';
7
+
8
+ export interface ArtifactLineageRecord {
9
+ artifactKind: ArtifactKind;
10
+ artifactId: string;
11
+ principleId: string;
12
+ ruleId: string | null;
13
+ sessionId: string;
14
+ sourceSnapshotRef: string;
15
+ sourcePainIds: string[];
16
+ sourceGateBlockIds: string[];
17
+ storagePath: string;
18
+ implementationId: string | null;
19
+ createdAt: string;
20
+ }
21
+
22
+ export interface CandidateArtifactLineageInput {
23
+ artifactId: string;
24
+ principleId: string;
25
+ ruleId: string;
26
+ sessionId: string;
27
+ sourceSnapshotRef: string;
28
+ sourcePainIds: string[];
29
+ sourceGateBlockIds: string[];
30
+ storagePath: string;
31
+ implementationId: string;
32
+ createdAt?: string;
33
+ }
34
+
35
+ function getLineageRegistryPath(workspaceDir: string): string {
36
+ return path.join(resolveNocturnalDir(workspaceDir, 'ROOT'), 'artifact-lineage.json');
37
+ }
38
+
39
+ function readArtifactLineageRegistry(workspaceDir: string): ArtifactLineageRecord[] {
40
+ const registryPath = getLineageRegistryPath(workspaceDir);
41
+ if (!fs.existsSync(registryPath)) {
42
+ return [];
43
+ }
44
+
45
+ try {
46
+ const content = fs.readFileSync(registryPath, 'utf-8');
47
+ return JSON.parse(content) as ArtifactLineageRecord[];
48
+ } catch {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ function writeArtifactLineageRegistry(
54
+ workspaceDir: string,
55
+ records: ArtifactLineageRecord[]
56
+ ): void {
57
+ const registryPath = getLineageRegistryPath(workspaceDir);
58
+ fs.mkdirSync(path.dirname(registryPath), { recursive: true });
59
+ const tmpPath = `${registryPath}.tmp`;
60
+ fs.writeFileSync(tmpPath, JSON.stringify(records, null, 2), 'utf-8');
61
+ fs.renameSync(tmpPath, registryPath);
62
+ }
63
+
64
+ export function appendArtifactLineageRecord(
65
+ workspaceDir: string,
66
+ record: ArtifactLineageRecord
67
+ ): ArtifactLineageRecord {
68
+ const registryPath = getLineageRegistryPath(workspaceDir);
69
+
70
+ return withLock(registryPath, () => {
71
+ const records = readArtifactLineageRegistry(workspaceDir);
72
+ const nextRecord: ArtifactLineageRecord = {
73
+ ...record,
74
+ sourcePainIds: [...record.sourcePainIds],
75
+ sourceGateBlockIds: [...record.sourceGateBlockIds],
76
+ storagePath: path.normalize(record.storagePath),
77
+ };
78
+ records.push(nextRecord);
79
+ writeArtifactLineageRegistry(workspaceDir, records);
80
+ return nextRecord;
81
+ });
82
+ }
83
+
84
+ export function listArtifactLineageRecords(
85
+ workspaceDir: string,
86
+ artifactKind?: ArtifactKind
87
+ ): ArtifactLineageRecord[] {
88
+ const records = readArtifactLineageRegistry(workspaceDir);
89
+ const filtered =
90
+ artifactKind === undefined
91
+ ? records
92
+ : records.filter((record) => record.artifactKind === artifactKind);
93
+
94
+ return filtered.sort(
95
+ (left, right) =>
96
+ new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime()
97
+ );
98
+ }
99
+
100
+ export function appendCandidateArtifactLineageRecord(
101
+ workspaceDir: string,
102
+ input: CandidateArtifactLineageInput
103
+ ): ArtifactLineageRecord {
104
+ return appendArtifactLineageRecord(workspaceDir, {
105
+ artifactKind: 'rule-implementation-candidate',
106
+ artifactId: input.artifactId,
107
+ principleId: input.principleId,
108
+ ruleId: input.ruleId,
109
+ sessionId: input.sessionId,
110
+ sourceSnapshotRef: input.sourceSnapshotRef,
111
+ sourcePainIds: input.sourcePainIds,
112
+ sourceGateBlockIds: input.sourceGateBlockIds,
113
+ storagePath: input.storagePath,
114
+ implementationId: input.implementationId,
115
+ createdAt: input.createdAt ?? new Date().toISOString(),
116
+ });
117
+ }
@@ -0,0 +1,257 @@
1
+ import type { NocturnalSessionSnapshot } from './nocturnal-trajectory-extractor.js';
2
+ import {
3
+ getPrincipleSubtree,
4
+ listImplementationsForRule,
5
+ listRuleImplementationsByState,
6
+ } from './principle-tree-ledger.js';
7
+ import type { LedgerRule } from './principle-tree-ledger.js';
8
+
9
+ export type ArtificerArtifactKind = 'rule-implementation-candidate';
10
+
11
+ export interface ArtificerLineageMetadata {
12
+ artifactKind: ArtificerArtifactKind;
13
+ sourceSnapshotRef: string;
14
+ sourcePainIds: string[];
15
+ sourceGateBlockIds: string[];
16
+ }
17
+
18
+ export interface ArtificerInput {
19
+ principleId: string;
20
+ ruleId: string;
21
+ snapshot: NocturnalSessionSnapshot;
22
+ scribeArtifact: {
23
+ sessionId: string;
24
+ badDecision: string;
25
+ betterDecision: string;
26
+ rationale: string;
27
+ sourceSnapshotRef: string;
28
+ };
29
+ lineage: ArtificerLineageMetadata;
30
+ }
31
+
32
+ export interface ArtificerOutput {
33
+ ruleId: string;
34
+ implementationType: 'code';
35
+ candidateSource: string;
36
+ helperUsage: string[];
37
+ expectedDecision: 'allow' | 'block' | 'requireApproval';
38
+ rationale: string;
39
+ lineage: ArtificerLineageMetadata;
40
+ }
41
+
42
+ export interface ArtificerTargetRuleScore {
43
+ ruleId: string;
44
+ score: number;
45
+ matchedSignals: string[];
46
+ }
47
+
48
+ export type ArtificerTargetRuleResolution =
49
+ | {
50
+ status: 'selected';
51
+ ruleId: string;
52
+ reason: 'single-rule' | 'evidence-winner';
53
+ scores: ArtificerTargetRuleScore[];
54
+ }
55
+ | {
56
+ status: 'skip';
57
+ reason:
58
+ | 'principle-not-found'
59
+ | 'no-rules'
60
+ | 'ambiguous-target-rule'
61
+ | 'no-deterministic-signal';
62
+ scores: ArtificerTargetRuleScore[];
63
+ };
64
+
65
+ export interface TrinityArtificerContext {
66
+ principleId: string;
67
+ resolution: ArtificerTargetRuleResolution;
68
+ eligible: boolean;
69
+ }
70
+
71
+ function tokenize(value: string | undefined): string[] {
72
+ if (!value) {
73
+ return [];
74
+ }
75
+
76
+ return value
77
+ .toLowerCase()
78
+ .split(/[^a-z0-9]+/i)
79
+ .map((token) => token.trim())
80
+ .filter((token) => token.length >= 3);
81
+ }
82
+
83
+ function countMatches(tokens: string[], haystacks: string[]): number {
84
+ return tokens.reduce((total, token) => {
85
+ if (haystacks.some((haystack) => haystack.includes(token))) {
86
+ return total + 1;
87
+ }
88
+ return total;
89
+ }, 0);
90
+ }
91
+
92
+ function scoreRule(
93
+ stateDir: string,
94
+ rule: LedgerRule,
95
+ snapshot: NocturnalSessionSnapshot
96
+ ): ArtificerTargetRuleScore {
97
+ const toolNames = snapshot.toolCalls.map((toolCall) => toolCall.toolName.toLowerCase());
98
+ const painReasons = snapshot.painEvents
99
+ .map((painEvent) => painEvent.reason?.toLowerCase())
100
+ .filter((reason): reason is string => typeof reason === 'string' && reason.length > 0);
101
+ const gateReasons = snapshot.gateBlocks.map((gateBlock) => gateBlock.reason.toLowerCase());
102
+ const ruleTokens = [
103
+ ...tokenize(rule.name),
104
+ ...tokenize(rule.description),
105
+ ...tokenize(rule.triggerCondition),
106
+ ...tokenize(rule.action),
107
+ ];
108
+
109
+ const gateMatches = countMatches(ruleTokens, gateReasons);
110
+ const painMatches = countMatches(ruleTokens, painReasons);
111
+ const toolMatches = countMatches(ruleTokens, toolNames);
112
+ const candidateImplCount = listRuleImplementationsByState(stateDir, rule.id, 'candidate').length;
113
+ const totalImplCount = listImplementationsForRule(stateDir, rule.id).length;
114
+ const implementationSignal = Math.max(totalImplCount - candidateImplCount, 0);
115
+ const score = gateMatches * 1000 + painMatches * 100 + toolMatches * 10 + implementationSignal;
116
+ const matchedSignals: string[] = [];
117
+
118
+ if (gateMatches > 0) {
119
+ matchedSignals.push(`gate:${gateMatches}`);
120
+ }
121
+ if (painMatches > 0) {
122
+ matchedSignals.push(`pain:${painMatches}`);
123
+ }
124
+ if (toolMatches > 0) {
125
+ matchedSignals.push(`tool:${toolMatches}`);
126
+ }
127
+ if (implementationSignal > 0) {
128
+ matchedSignals.push(`impl:${implementationSignal}`);
129
+ }
130
+
131
+ return {
132
+ ruleId: rule.id,
133
+ score,
134
+ matchedSignals,
135
+ };
136
+ }
137
+
138
+ export function resolveArtificerTargetRule(
139
+ stateDir: string,
140
+ principleId: string,
141
+ snapshot: NocturnalSessionSnapshot
142
+ ): ArtificerTargetRuleResolution {
143
+ const subtree = getPrincipleSubtree(stateDir, principleId);
144
+ if (!subtree) {
145
+ return {
146
+ status: 'skip',
147
+ reason: 'principle-not-found',
148
+ scores: [],
149
+ };
150
+ }
151
+
152
+ const eligibleRules = subtree.rules
153
+ .map((entry) => entry.rule)
154
+ .filter((rule) => rule.status !== 'retired');
155
+
156
+ if (eligibleRules.length === 0) {
157
+ return {
158
+ status: 'skip',
159
+ reason: 'no-rules',
160
+ scores: [],
161
+ };
162
+ }
163
+
164
+ if (eligibleRules.length === 1) {
165
+ return {
166
+ status: 'selected',
167
+ ruleId: eligibleRules[0].id,
168
+ reason: 'single-rule',
169
+ scores: [
170
+ {
171
+ ruleId: eligibleRules[0].id,
172
+ score: 1,
173
+ matchedSignals: ['single-rule'],
174
+ },
175
+ ],
176
+ };
177
+ }
178
+
179
+ const scores = eligibleRules.map((rule) => scoreRule(stateDir, rule, snapshot));
180
+ const sorted = [...scores].sort((left, right) => {
181
+ if (right.score !== left.score) {
182
+ return right.score - left.score;
183
+ }
184
+ return left.ruleId.localeCompare(right.ruleId);
185
+ });
186
+ const [winner, runnerUp] = sorted;
187
+
188
+ if (!winner || winner.score === 0) {
189
+ return {
190
+ status: 'skip',
191
+ reason: 'no-deterministic-signal',
192
+ scores: sorted,
193
+ };
194
+ }
195
+
196
+ if (runnerUp && runnerUp.score === winner.score) {
197
+ return {
198
+ status: 'skip',
199
+ reason: 'ambiguous-target-rule',
200
+ scores: sorted,
201
+ };
202
+ }
203
+
204
+ return {
205
+ status: 'selected',
206
+ ruleId: winner.ruleId,
207
+ reason: 'evidence-winner',
208
+ scores: sorted,
209
+ };
210
+ }
211
+
212
+ export function shouldRunArtificer(
213
+ snapshot: NocturnalSessionSnapshot,
214
+ resolution: ArtificerTargetRuleResolution,
215
+ minimumSignalCount = 2
216
+ ): boolean {
217
+ if (resolution.status !== 'selected') {
218
+ return false;
219
+ }
220
+
221
+ const signalCount =
222
+ snapshot.stats.totalPainEvents +
223
+ snapshot.stats.totalGateBlocks +
224
+ snapshot.stats.failureCount;
225
+
226
+ return signalCount >= minimumSignalCount;
227
+ }
228
+
229
+ export function parseArtificerOutput(payload: string): ArtificerOutput | null {
230
+ try {
231
+ const parsed = JSON.parse(payload) as Partial<ArtificerOutput>;
232
+ if (
233
+ typeof parsed.ruleId !== 'string' ||
234
+ typeof parsed.candidateSource !== 'string' ||
235
+ !Array.isArray(parsed.helperUsage) ||
236
+ typeof parsed.expectedDecision !== 'string' ||
237
+ typeof parsed.rationale !== 'string' ||
238
+ !parsed.lineage
239
+ ) {
240
+ return null;
241
+ }
242
+
243
+ return {
244
+ ruleId: parsed.ruleId,
245
+ implementationType: 'code',
246
+ candidateSource: parsed.candidateSource,
247
+ helperUsage: parsed.helperUsage.filter(
248
+ (value): value is string => typeof value === 'string'
249
+ ),
250
+ expectedDecision: parsed.expectedDecision,
251
+ rationale: parsed.rationale,
252
+ lineage: parsed.lineage,
253
+ };
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
@@ -158,7 +158,7 @@ export function scoreCandidate(
158
158
  let boundedness = 0.5;
159
159
  // Specific: mentions specific targets (files, tools, etc.)
160
160
  const betterDecisionStr = candidate.betterDecision ?? '';
161
- const hasSpecificTarget = /[a-zA-Z0-9_\-]+\.(ts|js|json|md|yml|yaml|sh|py|go|rs)/.test(betterDecisionStr);
161
+ const hasSpecificTarget = /[a-zA-Z0-9_.]+\.(ts|js|json|md|yml|yaml|sh|py|go|rs)/.test(betterDecisionStr);
162
162
  if (hasSpecificTarget) boundedness += 0.2;
163
163
  // Not too generic
164
164
  const genericPatterns = [
@@ -241,6 +241,7 @@ export function checkThresholds(
241
241
  * @param weights - Scoring weights
242
242
  * @returns All scored and ranked candidates
243
243
  */
244
+ // eslint-disable-next-line @typescript-eslint/max-params -- Reason: candidate ranking requires candidates + judgments + thresholds + weights - refactoring would break API
244
245
  export function rankCandidates(
245
246
  candidates: DreamerCandidate[],
246
247
  judgments: PhilosopherJudgment[],
@@ -327,6 +328,7 @@ export function rankCandidates(
327
328
  * @param weights - Scoring weights
328
329
  * @returns Tournament result with winner
329
330
  */
331
+ // eslint-disable-next-line @typescript-eslint/max-params -- Reason: tournament requires candidates + judgments + thresholds + weights - refactoring would break API
330
332
  export function runTournament(
331
333
  candidates: DreamerCandidate[],
332
334
  judgments: PhilosopherJudgment[],
@@ -376,7 +378,7 @@ export function runTournament(
376
378
  }
377
379
 
378
380
  // Winner is rank 1
379
- const winner = eligible[0];
381
+ const [winner] = eligible;
380
382
 
381
383
  trace.push({
382
384
  step: 'Winner Selected',