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
@@ -0,0 +1,463 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import type { PDTaskSpec } from './pd-task-types.js';
5
+ import { BUILTIN_PD_TASKS } from './pd-task-types.js';
6
+ import { readTasks, writeTasks } from './pd-task-store.js';
7
+ import { withLockAsync } from '../utils/file-lock.js';
8
+
9
+ const CRON_STORE_PATH = path.join(
10
+ os.homedir(),
11
+ '.openclaw',
12
+ 'cron',
13
+ 'jobs.json',
14
+ );
15
+
16
+ export interface CronJobState {
17
+ nextRunAtMs?: number;
18
+ runningAtMs?: number;
19
+ lastRunAtMs?: number;
20
+ lastRunStatus?: string;
21
+ lastStatus?: 'ok' | 'error' | 'skipped';
22
+ lastError?: string;
23
+ lastDurationMs?: number;
24
+ consecutiveErrors?: number;
25
+ }
26
+
27
+ export interface CronJob {
28
+ id: string;
29
+ name: string;
30
+ agentId?: string;
31
+ description?: string;
32
+ enabled: boolean;
33
+ deleteAfterRun?: boolean;
34
+ createdAtMs: number;
35
+ updatedAtMs: number;
36
+ schedule: { kind: 'every'; everyMs: number } | { kind: 'at'; at: string } | { kind: 'cron'; expr: string; tz?: string };
37
+ sessionTarget: string;
38
+ wakeMode: string;
39
+ payload: { kind: 'systemEvent'; text: string } | { kind: 'agentTurn'; message: string; model?: string; thinking?: string; timeoutSeconds?: number; lightContext?: boolean; toolsAllow?: string[] };
40
+ delivery?: { mode: string; channel?: string; to?: string };
41
+ state: CronJobState;
42
+ metadata?: Record<string, string>;
43
+ }
44
+
45
+ export interface CronStoreFile {
46
+ version: number;
47
+ jobs: CronJob[];
48
+ }
49
+
50
+ export type DiffActionType = 'CREATE' | 'UPDATE' | 'DISABLE' | 'ORPHAN' | 'SKIP';
51
+
52
+ export interface DiffAction {
53
+ type: DiffActionType;
54
+ task?: PDTaskSpec;
55
+ job?: CronJob;
56
+ }
57
+
58
+ export interface ReconcileError {
59
+ taskId: string;
60
+ message: string;
61
+ }
62
+
63
+ export interface ReconcileResult {
64
+ created: string[];
65
+ updated: string[];
66
+ skipped: string[];
67
+ orphaned: string[];
68
+ errors: ReconcileError[];
69
+ }
70
+
71
+
72
+ export interface ReconcileOptions {
73
+ dryRun?: boolean;
74
+ workspaceDir: string;
75
+ /* eslint-disable no-unused-vars -- Reason: logger callback param names intentionally unused - callbacks only invoked for side effects */
76
+ logger?: { info?: (_: string) => void; warn?: (_: string) => void };
77
+ /* eslint-enable no-unused-vars */
78
+ }
79
+
80
+ /* eslint-disable no-unused-vars -- Reason: logger callbacks have unused param names in type */
81
+ async function readCronStore(logger?: { info?: (_: string) => void; warn?: (_: string) => void }): Promise<CronStoreFile> {
82
+ /* eslint-enable no-unused-vars */
83
+ if (!fs.existsSync(CRON_STORE_PATH)) {
84
+ logger?.info?.(`[PD:Reconciler] cron/jobs.json not found, starting with empty store`);
85
+ return { version: 1, jobs: [] };
86
+ }
87
+ try {
88
+ const raw = fs.readFileSync(CRON_STORE_PATH, 'utf-8');
89
+ const store = JSON.parse(raw) as CronStoreFile;
90
+ logger?.info?.(`[PD:Reconciler] Loaded cron/jobs.json: ${store.jobs.length} jobs`);
91
+ return store;
92
+ } catch (err) {
93
+ logger?.warn?.(`[PD:Reconciler] Failed to parse cron/jobs.json: ${String(err)}`);
94
+ return { version: 1, jobs: [] };
95
+ }
96
+ }
97
+
98
+ async function writeCronStore(store: CronStoreFile): Promise<void> {
99
+ await withLockAsync(CRON_STORE_PATH, async () => {
100
+ const tmpPath = CRON_STORE_PATH + '.tmp';
101
+ fs.writeFileSync(tmpPath, JSON.stringify(store, null, 2), 'utf-8');
102
+ fs.renameSync(tmpPath, CRON_STORE_PATH);
103
+ });
104
+ }
105
+
106
+ function diff(declared: PDTaskSpec[], actual: CronJob[]): DiffAction[] {
107
+ const actions: DiffAction[] = [];
108
+ const actualByName = new Map<string, CronJob>();
109
+ for (const job of actual) {
110
+ actualByName.set(job.name, job);
111
+ }
112
+
113
+ for (const task of declared) {
114
+ const job = actualByName.get(task.name);
115
+
116
+ if (!job) {
117
+ if (task.enabled) {
118
+ actions.push({ type: 'CREATE', task });
119
+ }
120
+ } else {
121
+ const pdVersion = job.metadata?.pdVersion;
122
+ if (!task.enabled) {
123
+ actions.push({ type: 'DISABLE', task, job });
124
+ } else if (!pdVersion || pdVersion !== task.version) {
125
+ actions.push({ type: 'UPDATE', task, job });
126
+ } else {
127
+ actions.push({ type: 'SKIP', task, job });
128
+ }
129
+ }
130
+ }
131
+
132
+ for (const job of actual) {
133
+ if (!job.name.startsWith('PD ')) continue;
134
+ const found = declared.find((t) => t.name === job.name);
135
+ if (!found) {
136
+ actions.push({ type: 'ORPHAN', job });
137
+ }
138
+ }
139
+
140
+ return actions;
141
+ }
142
+
143
+ function buildCronJob(
144
+ task: PDTaskSpec,
145
+ nowMs: number,
146
+ // eslint-disable-next-line no-unused-vars -- logger callback param unused in type
147
+ logger?: { info?: (_: string) => void },
148
+ ): CronJob {
149
+ logger?.info?.(`[PD:Reconciler] Building cron job: ${task.name} (id=${task.id}, interval=${task.schedule.everyMs}ms)`);
150
+ return {
151
+ id: `pd-${task.id}-${nowMs}`,
152
+ name: task.name,
153
+ agentId: task.agentId || 'main',
154
+ description: task.description,
155
+ enabled: task.enabled,
156
+ schedule: { kind: 'every', everyMs: task.schedule.everyMs },
157
+ sessionTarget: 'isolated',
158
+ wakeMode: 'now',
159
+ payload: {
160
+ kind: 'agentTurn',
161
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: buildTaskPrompt is defined later in this file, called here for organizational reasons
162
+ message: buildTaskPrompt(task, logger),
163
+ lightContext: task.execution.lightContext ?? true,
164
+ timeoutSeconds: task.execution.timeoutSeconds ?? 120,
165
+ toolsAllow: task.execution.toolsAllow,
166
+ },
167
+ delivery: { mode: task.delivery.mode },
168
+ createdAtMs: nowMs,
169
+ updatedAtMs: nowMs,
170
+ state: {
171
+ nextRunAtMs: nowMs + task.schedule.everyMs,
172
+ },
173
+ metadata: {
174
+ pdVersion: task.version,
175
+ pdTaskId: task.id,
176
+ },
177
+ };
178
+ }
179
+
180
+ // eslint-disable-next-line no-unused-vars -- logger callback param unused in type
181
+ function buildTaskPrompt(task: PDTaskSpec, logger?: { info?: (_: string) => void }): string {
182
+ if (task.id === 'empathy-optimizer') {
183
+ logger?.info?.(`[PD:Reconciler] Building empathy optimizer prompt`);
184
+ return `You are the Principles Disciple Empathy Keyword Optimizer.
185
+
186
+ ## TASK
187
+ Analyze the current empathy keyword store and recent user message logs to:
188
+ 1. Discover NEW frustration expressions not in the current store
189
+ 2. ADJUST weights of existing terms based on actual hit frequency
190
+ 3. REMOVE terms that produce too many false positives
191
+
192
+ ## WORKFLOW (execute in order)
193
+
194
+ ### Step 1: Read current keyword store
195
+ Use read_file to load:
196
+ \`~/.openclaw/workspace-main/.state/empathy_keywords.json\`
197
+
198
+ Examine the "terms" object. For each term note:
199
+ - weight (0.1-0.9): higher = stronger frustration signal
200
+ - hitCount: how many times it matched
201
+ - falsePositiveRate (0.05-0.5): how often it's a false alarm
202
+
203
+ ### Step 2: Read recent message logs
204
+ Use search_file_content to scan:
205
+ \`~/.openclaw/workspace-main/.state/logs/events.jsonl\`
206
+
207
+ Look for user messages containing frustration signals:
208
+ - Negation: "不对", "错了", "不行", "重做"
209
+ - Anger: "垃圾", "蠢", "废物", "白做"
210
+ - Disappointment: "不行啊", "还是不对", "没解决"
211
+ - Escalation: "你到底在干什么", "你确定吗", "what are you doing"
212
+
213
+ ### Step 3: Write updated keyword store
214
+ Use write_file to save the updated store back to:
215
+ \`~/.openclaw/workspace-main/.state/empathy_keywords.json\`
216
+
217
+ The file format is:
218
+ \`\`\`json
219
+ {
220
+ "version": 1,
221
+ "lastUpdated": "ISO timestamp",
222
+ "lastOptimizedAt": "ISO timestamp",
223
+ "terms": {
224
+ "TERM": {
225
+ "weight": 0.5,
226
+ "source": "seed|llm_discovered|user_reported",
227
+ "hitCount": 0,
228
+ "falsePositiveRate": 0.15
229
+ }
230
+ },
231
+ "stats": {
232
+ "totalHits": 0,
233
+ "totalFalsePositives": 0,
234
+ "optimizationCount": 1
235
+ }
236
+ }
237
+ \`\`\`
238
+
239
+ **IMPORTANT**: You MUST use the write_file tool. Do NOT just return JSON in your response.
240
+
241
+ ### Step 4: Report summary
242
+ After writing the file, reply with a brief summary:
243
+ \`\`\`
244
+ Empathy keyword optimization complete:
245
+ - Added: N new terms (list them)
246
+ - Updated: M terms (list changes)
247
+ - Removed: K terms (list them)
248
+ - Total terms in store: X
249
+ \`\`\`
250
+
251
+ ## RULES
252
+ - ADD: If you find frustration expressions in logs NOT in current terms
253
+ - source = "llm_discovered", discoveredAt = current ISO timestamp
254
+ - weight: 0.5-0.7 for new terms (start conservative)
255
+ - falsePositiveRate: 0.2-0.3 (uncertain until validated)
256
+ - UPDATE: Adjust based on evidence
257
+ - High hitCount + low FPR → increase weight
258
+ - Low hitCount + high FPR → decrease weight
259
+ - Keep weight in 0.1-0.9, FPR in 0.05-0.5
260
+ - REMOVE: If hitCount=0 AND falsePositiveRate > 0.3 AND term is clearly generic
261
+ - Don't remove terms that might be valid but rare
262
+ - PRESERVE: Keep existing hitCount, lastHitAt, discoveredAt for existing terms
263
+ - Bump stats.optimizationCount by 1
264
+ - Set lastOptimizedAt to current ISO timestamp
265
+
266
+ ## EXAMPLES
267
+ - "不对" has hitCount=50 → increase weight from 0.5 to 0.7
268
+ - "呵呵" has hitCount=0, FPR=0.4, generic term → REMOVE
269
+ - User says "烦死了" in logs, not in store → ADD weight=0.6, FPR=0.25`;
270
+ }
271
+ return task.description;
272
+ }
273
+
274
+ export async function reconcilePDTasks(
275
+ workspaceDir: string,
276
+ options?: Partial<ReconcileOptions>,
277
+ ): Promise<ReconcileResult> {
278
+ const dryRun = options?.dryRun ?? false;
279
+ const logger = options?.logger ?? console;
280
+ const nowMs = Date.now();
281
+
282
+ const result: ReconcileResult = { created: [], updated: [], skipped: [], orphaned: [], errors: [] };
283
+
284
+ const storedTasks = readTasks(workspaceDir);
285
+ const storedById = new Map(storedTasks.map((t) => [t.id, t]));
286
+ const declared: PDTaskSpec[] = BUILTIN_PD_TASKS.map((t) => {
287
+ const stored = storedById.get(t.id);
288
+ if (stored) {
289
+ return { ...t, meta: { ...t.meta, ...stored.meta } };
290
+ }
291
+ return { ...t, meta: { createdAtMs: nowMs } };
292
+ });
293
+
294
+ const cronStore = await readCronStore(logger);
295
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: healthCheck is defined later in this file, called here for organizational reasons
296
+ const healthUpdated = healthCheck(declared, cronStore, logger);
297
+ const actions = diff(healthUpdated, cronStore.jobs);
298
+
299
+ for (const action of actions) {
300
+ switch (action.type) {
301
+ case 'CREATE':
302
+ if (action.task) {
303
+ if (!dryRun) {
304
+ const job = buildCronJob(action.task, nowMs, logger);
305
+ cronStore.jobs.push(job);
306
+ logger.info?.(`[PD:Reconciler] Created job: ${action.task.name}`);
307
+ }
308
+ result.created.push(action.task.name);
309
+ }
310
+ break;
311
+ case 'UPDATE':
312
+ if (action.task && action.job) {
313
+ if (!dryRun) {
314
+ const idx = cronStore.jobs.indexOf(action.job);
315
+ const newJob = buildCronJob(action.task, nowMs, logger);
316
+ newJob.id = action.job.id;
317
+ // Preserve original state — only CronService should recalculate nextRunAtMs
318
+ newJob.state = {
319
+ ...action.job.state,
320
+ nextRunAtMs: undefined, // Let CronService recalculate
321
+ };
322
+ cronStore.jobs[idx] = newJob;
323
+ logger.info?.(`[PD:Reconciler] Updated job: ${action.task.name}`);
324
+ }
325
+ result.updated.push(action.task.name);
326
+ }
327
+ break;
328
+ case 'DISABLE':
329
+ if (action.job) {
330
+ if (!dryRun) {
331
+ action.job.enabled = false;
332
+ action.job.updatedAtMs = nowMs;
333
+ }
334
+ logger.warn?.(`[PD:Reconciler] Disabled job: ${action.job.name}`);
335
+ }
336
+ break;
337
+ case 'ORPHAN':
338
+ if (action.job) {
339
+ logger.warn?.(`[PD:Reconciler] Orphaned job (no declaration): ${action.job.name}`);
340
+ result.orphaned.push(action.job.name);
341
+ }
342
+ break;
343
+ case 'SKIP':
344
+ if (action.task) {
345
+ result.skipped.push(action.task.name);
346
+ }
347
+ break;
348
+ }
349
+ }
350
+
351
+ if (!dryRun && (result.created.length > 0 || result.updated.length > 0)) {
352
+ await writeCronStore(cronStore);
353
+ logger.info?.(`[PD:Reconciler] Wrote cron/jobs.json: ${cronStore.jobs.length} total jobs`);
354
+ for (const job of cronStore.jobs) {
355
+ if (job.name.startsWith('PD ')) {
356
+ logger.info?.(`[PD:Reconciler] PD job: name=${job.name}, enabled=${job.enabled}, schedule=${JSON.stringify(job.schedule)}, nextRun=${job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : 'TBD'}`);
357
+ }
358
+ }
359
+ }
360
+
361
+ if (!dryRun && declared.length > 0) {
362
+ await writeTasks(workspaceDir, declared);
363
+ }
364
+
365
+ return result;
366
+ }
367
+
368
+
369
+ function healthCheck(
370
+ tasks: PDTaskSpec[],
371
+ cronStore: CronStoreFile,
372
+ /* eslint-disable no-unused-vars -- Reason: callback type signature parameters */
373
+ logger: { info?: (_msg: string) => void; warn?: (_msg: string) => void },
374
+ /* eslint-enable no-unused-vars */
375
+ ): PDTaskSpec[] {
376
+ const jobByName = new Map(cronStore.jobs.map((j) => [j.name, j]));
377
+
378
+ for (const task of tasks) {
379
+ const job = jobByName.get(task.name);
380
+ if (!job) continue;
381
+
382
+ const errors = job.state.consecutiveErrors ?? 0;
383
+ const lastError = job.state.lastError ?? '';
384
+ const isTimeout = lastError.includes('timed out') || lastError.includes('timeout');
385
+
386
+ // Auto-increase timeout on timeout error (exponential backoff: 2x, max 1800s)
387
+ if (isTimeout && errors > 0 && job.payload.kind === 'agentTurn') {
388
+ const currentTimeout = job.payload.timeoutSeconds ?? 120;
389
+ const newTimeout = Math.min(1800, currentTimeout * 2);
390
+ if (newTimeout > currentTimeout) {
391
+ job.payload.timeoutSeconds = newTimeout;
392
+ job.state.consecutiveErrors = 0;
393
+ job.state.lastError = undefined;
394
+ logger.info?.(`[PD:Reconciler] Auto-increased timeout for '${task.name}': ${currentTimeout}s → ${newTimeout}s (was ${errors} consecutive timeouts)`);
395
+ }
396
+ }
397
+
398
+ // Auto-disable only for non-timeout errors after 3 consecutive failures
399
+ if (errors >= 3 && !isTimeout && !task.meta?.autoDisabled) {
400
+ if (!task.meta) task.meta = {};
401
+ task.meta.autoDisabled = true;
402
+ task.meta.autoDisabledAt = Date.now();
403
+ task.meta.autoDisabledReason = `consecutiveErrors=${errors}`;
404
+ logger.warn?.(`[PD:Reconciler] Auto-disabled task '${task.id}' due to ${errors} consecutive errors: ${lastError.substring(0, 80)}`);
405
+ }
406
+
407
+ // Reset consecutiveErrors on non-error runs
408
+ if (errors === 0 && task.meta?.autoDisabled) {
409
+ logger.info?.(`[PD:Reconciler] Task '${task.id}' was previously auto-disabled but has 0 consecutive errors now`);
410
+ }
411
+
412
+ if (task.meta) {
413
+ task.meta.consecutiveFailCount = errors;
414
+ if (job.state.lastRunAtMs) {
415
+ task.meta.lastFailedAtMs = job.state.lastRunAtMs;
416
+ }
417
+ }
418
+ }
419
+ return tasks;
420
+ }
421
+
422
+ export async function trigger(
423
+ taskId: string,
424
+ workspaceDir: string,
425
+ options?: { force?: boolean },
426
+ ): Promise<{ ok: boolean; error?: string }> {
427
+ const tasks = readTasks(workspaceDir);
428
+ const task = tasks.find((t) => t.id === taskId);
429
+ if (!task) {
430
+ return { ok: false, error: `Task '${taskId}' not found` };
431
+ }
432
+ if (task.meta?.autoDisabled && !options?.force) {
433
+ return { ok: false, error: 'Task is auto-disabled. Use force=true to override.' };
434
+ }
435
+
436
+ const log = (msg: string) => console.info(`[PD:Trigger] ${msg}`);
437
+ const cronStore = await readCronStore({ info: log, warn: log });
438
+ const nowMs = Date.now();
439
+ const existingJob = cronStore.jobs.find((j) => j.name === task.name);
440
+
441
+ if (existingJob) {
442
+ log(`Manually triggering existing job: ${task.name} (id=${existingJob.id})`);
443
+ existingJob.enabled = true;
444
+ existingJob.updatedAtMs = nowMs;
445
+ existingJob.state.nextRunAtMs = nowMs;
446
+ existingJob.deleteAfterRun = undefined;
447
+ } else {
448
+ log(`Creating new job for manual trigger: ${task.name}`);
449
+ const newJob = buildCronJob(task, nowMs, { info: log });
450
+ newJob.enabled = true;
451
+ newJob.state.nextRunAtMs = nowMs;
452
+ cronStore.jobs.push(newJob);
453
+ }
454
+
455
+ if (!task.meta) task.meta = {};
456
+ task.meta.lastTriggeredAtMs = nowMs;
457
+ task.meta.lastTriggerStatus = 'pending';
458
+
459
+ await writeCronStore(cronStore);
460
+ await writeTasks(workspaceDir, tasks);
461
+ log(`Trigger complete: nextRunAt=${nowMs}, will run on next cron cycle`);
462
+ return { ok: true };
463
+ }
@@ -0,0 +1,42 @@
1
+ import type { OpenClawPluginService, OpenClawPluginServiceContext } from '../openclaw-sdk.js';
2
+ import { reconcilePDTasks } from './pd-task-reconciler.js';
3
+
4
+ export const PDTaskService: OpenClawPluginService = {
5
+ id: 'principles-disciple-task-manager',
6
+
7
+ async start(ctx: OpenClawPluginServiceContext): Promise<void> {
8
+ const {workspaceDir} = ctx;
9
+ if (!workspaceDir) {
10
+ ctx.logger?.warn?.(`[PD:TaskManager] No workspaceDir, skipping PD task reconciliation`);
11
+ return;
12
+ }
13
+
14
+ const {logger} = ctx;
15
+ logger.info?.(`[PD:TaskManager] Starting PD task reconciliation...`);
16
+
17
+ try {
18
+ const result = await reconcilePDTasks(workspaceDir, { logger });
19
+ logger.info?.(
20
+ `[PD:TaskManager] Reconcile complete: +${result.created.length} ~${result.updated.length} =${result.skipped.length} orphan=${result.orphaned.length}`,
21
+ );
22
+ if (result.created.length > 0) {
23
+ logger.info?.(`[PD:TaskManager] Created jobs: ${result.created.join(', ')}`);
24
+ }
25
+ if (result.updated.length > 0) {
26
+ logger.info?.(`[PD:TaskManager] Updated jobs: ${result.updated.join(', ')}`);
27
+ }
28
+ if (result.errors.length > 0) {
29
+ logger.warn?.(
30
+ `[PD:TaskManager] Reconcile errors: ${result.errors.map((e) => e.message).join(', ')}`,
31
+ );
32
+ }
33
+ } catch (err) {
34
+ logger.warn?.(`[PD:TaskManager] Reconcile failed: ${String(err)}`);
35
+ }
36
+ },
37
+
38
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: stop method required by service interface but no cleanup needed
39
+ stop(_ctx: OpenClawPluginServiceContext): void {
40
+ /* intentionally empty - no cleanup required for this service */
41
+ },
42
+ };
@@ -0,0 +1,77 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { PDTaskSpec } from './pd-task-types.js';
4
+ import { withLockAsync } from '../utils/file-lock.js';
5
+
6
+ const PD_TASKS_FILENAME = 'pd_tasks.json';
7
+
8
+ function resolvePdTasksPath(workspaceDir: string): string {
9
+ return path.join(workspaceDir, '.state', PD_TASKS_FILENAME);
10
+ }
11
+
12
+ function ensureStateDir(workspaceDir: string): void {
13
+ const stateDir = path.join(workspaceDir, '.state');
14
+ if (!fs.existsSync(stateDir)) {
15
+ fs.mkdirSync(stateDir, { recursive: true });
16
+ }
17
+ }
18
+
19
+ export function readTasks(workspaceDir: string): PDTaskSpec[] {
20
+ const filePath = resolvePdTasksPath(workspaceDir);
21
+ if (!fs.existsSync(filePath)) {
22
+ return [];
23
+ }
24
+ try {
25
+ const raw = fs.readFileSync(filePath, 'utf-8');
26
+ const parsed = JSON.parse(raw);
27
+ if (Array.isArray(parsed)) {
28
+ return parsed as PDTaskSpec[];
29
+ }
30
+ return [];
31
+ } catch (err) {
32
+ console.warn(`[PD:TaskStore] Failed to parse ${PD_TASKS_FILENAME}: ${String(err)}`);
33
+ return [];
34
+ }
35
+ }
36
+
37
+ export async function writeTasks(workspaceDir: string, tasks: PDTaskSpec[]): Promise<void> {
38
+ const filePath = resolvePdTasksPath(workspaceDir);
39
+ ensureStateDir(workspaceDir);
40
+
41
+ await withLockAsync(filePath, async () => {
42
+ const tmpPath = filePath + '.tmp';
43
+ fs.writeFileSync(tmpPath, JSON.stringify(tasks, null, 2), 'utf-8');
44
+ fs.renameSync(tmpPath, filePath);
45
+ });
46
+ }
47
+
48
+ export function initTaskMeta(task: PDTaskSpec): PDTaskSpec {
49
+ if (!task.meta) {
50
+ task.meta = {};
51
+ }
52
+ if (!task.meta.createdAtMs) {
53
+ task.meta.createdAtMs = Date.now();
54
+ }
55
+ return task;
56
+ }
57
+
58
+ // eslint-disable-next-line @typescript-eslint/max-params -- Reason: sync meta update requires task + status + optional jobId + error - refactoring would break API
59
+ export function updateSyncMeta(
60
+ task: PDTaskSpec,
61
+ status: 'ok' | 'error',
62
+ jobId?: string,
63
+ error?: string,
64
+ ): PDTaskSpec {
65
+ if (!task.meta) {
66
+ task.meta = {};
67
+ }
68
+ task.meta.lastSyncedAtMs = Date.now();
69
+ task.meta.lastSyncStatus = status;
70
+ if (jobId) {
71
+ task.meta.lastSyncedJobId = jobId;
72
+ }
73
+ if (error) {
74
+ task.meta.lastSyncError = error;
75
+ }
76
+ return task;
77
+ }