principles-disciple 1.80.0 → 1.82.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 (64) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/commands/strategy.ts +0 -18
  4. package/src/i18n/commands.ts +0 -12
  5. package/src/index.ts +47 -22
  6. package/src/service/correction-observer-service.ts +200 -0
  7. package/src/service/evolution-worker.ts +2 -123
  8. package/templates/langs/en/core/BOOTSTRAP.md +4 -18
  9. package/templates/langs/en/skills/bootstrap-tools/SKILL.md +1 -1
  10. package/templates/langs/en/skills/init-strategy/SKILL.md +1 -1
  11. package/templates/langs/en/skills/pd-mentor/SKILL.md +8 -23
  12. package/templates/langs/zh/core/BOOTSTRAP.md +2 -15
  13. package/templates/langs/zh/skills/bootstrap-tools/SKILL.md +1 -1
  14. package/templates/langs/zh/skills/init-strategy/SKILL.md +1 -1
  15. package/templates/langs/zh/skills/pd-mentor/SKILL.md +7 -22
  16. package/tests/commands/strategy.test.ts +3 -18
  17. package/tests/service/correction-observer-service.test.ts +331 -0
  18. package/tests/service/evolution-worker.correction-observer.test.ts +41 -164
  19. package/templates/langs/en/skills/ai-sprint-orchestration/EXAMPLES.md +0 -63
  20. package/templates/langs/en/skills/ai-sprint-orchestration/REFERENCE.md +0 -136
  21. package/templates/langs/en/skills/ai-sprint-orchestration/SKILL.md +0 -67
  22. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +0 -143
  23. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +0 -107
  24. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +0 -107
  25. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +0 -95
  26. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +0 -98
  27. package/templates/langs/en/skills/ai-sprint-orchestration/references/workflow-v1-acceptance-checklist.md +0 -58
  28. package/templates/langs/en/skills/ai-sprint-orchestration/references/workflow-v1.4-work-unit-handoff.md +0 -190
  29. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/archive.mjs +0 -310
  30. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/contract-enforcement.mjs +0 -683
  31. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/decision.mjs +0 -604
  32. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/state-store.mjs +0 -32
  33. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +0 -707
  34. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +0 -3455
  35. package/templates/langs/en/skills/evolve-system/SKILL.md +0 -46
  36. package/templates/langs/en/skills/manage-okr/SKILL.md +0 -96
  37. package/templates/langs/en/skills/pd-daily/SKILL.md +0 -199
  38. package/templates/langs/en/skills/pd-grooming/SKILL.md +0 -46
  39. package/templates/langs/zh/skills/ai-sprint-orchestration/EXAMPLES.md +0 -63
  40. package/templates/langs/zh/skills/ai-sprint-orchestration/REFERENCE.md +0 -136
  41. package/templates/langs/zh/skills/ai-sprint-orchestration/SKILL.md +0 -67
  42. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +0 -143
  43. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +0 -107
  44. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +0 -107
  45. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +0 -111
  46. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +0 -95
  47. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +0 -98
  48. package/templates/langs/zh/skills/ai-sprint-orchestration/references/workflow-v1-acceptance-checklist.md +0 -58
  49. package/templates/langs/zh/skills/ai-sprint-orchestration/references/workflow-v1.4-work-unit-handoff.md +0 -190
  50. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +0 -2
  51. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/archive.mjs +0 -310
  52. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/contract-enforcement.mjs +0 -683
  53. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/decision.mjs +0 -604
  54. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/state-store.mjs +0 -32
  55. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +0 -707
  56. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +0 -3455
  57. package/templates/langs/zh/skills/ai-sprint-orchestration/test/archive.test.mjs +0 -230
  58. package/templates/langs/zh/skills/ai-sprint-orchestration/test/contract-enforcement.test.mjs +0 -672
  59. package/templates/langs/zh/skills/ai-sprint-orchestration/test/decision.test.mjs +0 -1321
  60. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +0 -1435
  61. package/templates/langs/zh/skills/evolve-system/SKILL.md +0 -46
  62. package/templates/langs/zh/skills/manage-okr/SKILL.md +0 -109
  63. package/templates/langs/zh/skills/pd-daily/SKILL.md +0 -283
  64. package/templates/langs/zh/skills/pd-grooming/SKILL.md +0 -46
@@ -25,16 +25,13 @@ I'm your intelligent mentor, helping you understand and use all features of Prin
25
25
 
26
26
  | Command | Purpose | Use Case |
27
27
  |---------|---------|----------|
28
- | `/pd-init` | Initialize strategy and OKRs | New project startup |
29
- | `/pd-okr` | Objectives and Key Results management | Weekly/monthly review |
28
+ | `/pd-init` | Initialize strategy | New project startup |
30
29
  | `/pd-bootstrap` | Environment tool scan and upgrade | Tool upgrade |
31
30
  | `/pd-research` | Initiate tool upgrade research | Deep research |
32
31
  | `/pd-thinking` | Manage mental models and candidates | Metacognition |
33
32
  | `/pd-evolve` | Execute full evolution loop | Bug fix |
34
- | `/pd-daily` | Configure and send evolution daily report | Daily review |
35
33
  | `/pd-evolution-status` | View trust score and security stage | Permission check |
36
34
  | `/pd-status` | View system status (GFI and Pain Dictionary) | Health check |
37
- | `/pd-grooming` | Workspace digital cleanup | Entropy reduction |
38
35
  | `/pd-help` | Get interactive command guidance | This skill |
39
36
 
40
37
  ---
@@ -46,7 +43,7 @@ I'm your intelligent mentor, helping you understand and use all features of Prin
46
43
  **Trigger**: User says "I just created a new project" or similar
47
44
 
48
45
  **Recommended Flow**:
49
- 1. `/pd-init` - Establish strategic vision and OKR framework
46
+ 1. `/pd-init` - Establish strategic vision
50
47
  2. `/pd-bootstrap` - Scan environment tools, get capability list
51
48
  3. `/pd-thinking` - Establish project's mental model baseline
52
49
 
@@ -71,26 +68,14 @@ I'm your intelligent mentor, helping you understand and use all features of Prin
71
68
  **Trigger**: User says "what did I do today", "check progress", "give me a report"
72
69
 
73
70
  **Recommended Flow**:
74
- 1. `/pd-daily` - Send today's evolution report
75
- 2. `/pd-evolution-status` - View current trust score
76
- 3. `/pd-okr` - Check OKR alignment
71
+ 1. `/pd-evolution-status` - View current trust score
72
+ 2. `/pd-status` - Check GFI and Pain Dictionary status
77
73
 
78
- **Script**: "Daily report in hand, evolution I command. Let me help you review today's achievements."
74
+ **Script**: "Let me help you review the current system status."
79
75
 
80
76
  ---
81
77
 
82
- ### Scenario 4: Messy Workspace
83
-
84
- **Trigger**: User says "project is too messy", "too many files", "need to organize"
85
-
86
- **Recommended Flow**:
87
- 1. `/pd-grooming` - Start workspace cleanup
88
-
89
- **Script**: "Digital cleanliness is a virtue. Let me help you reduce entropy."
90
-
91
- ---
92
-
93
- ### Scenario 5: Permission or Security Related
78
+ ### Scenario 4: Permission or Security Related
94
79
 
95
80
  **Trigger**: User says "not enough permissions", "blocked", "security level"
96
81
 
@@ -186,8 +171,8 @@ For complex scenarios, combine multiple skills:
186
171
  | Scenario | Combined Flow |
187
172
  |----------|---------------|
188
173
  | Major refactor | `/pd-evolve` → `deductive-audit` → execute |
189
- | System optimization | `/pd-status` → `evolve-system` → `root-cause` |
190
- | Project review | `/pd-daily` → `/pd-okr` → `reflection-log` |
174
+ | System optimization | `/pd-status` → `root-cause` → optimize |
175
+ | Project review | `/pd-evolution-status` → `/pd-status` → `reflection-log` |
191
176
 
192
177
  ### Internal Skill Calls
193
178
 
@@ -162,20 +162,7 @@ memory/
162
162
 
163
163
  如果用户同意,**执行以下命令:**
164
164
 
165
- ### 1. 熵减巡检(每天凌晨 2 点)
166
-
167
- **功能:** 每天清理工作区临时文件,保持项目整洁。
168
-
169
- ```bash
170
- openclaw cron add --name "pd-grooming-daily" \
171
- --cron "0 2 * * *" --tz "UTC" \
172
- --session isolated \
173
- --light-context \
174
- --no-deliver \
175
- --message '执行 pd-grooming 技能:检查工作区根目录,清理临时文件和数字垃圾。严格遵循安全红线,不要删除业务代码。'
176
- ```
177
-
178
- ### 2. 环境健康检查(每 4 小时)
165
+ ### 1. 环境健康检查(每 4 小时)
179
166
 
180
167
  **功能:** 验证核心工具(rg, node, python)是否可用,异常时告警。
181
168
 
@@ -220,7 +207,7 @@ openclaw cron add --name "weekly-governance" \
220
207
  --cron "0 0 * * 0" --tz "UTC" \
221
208
  --session isolated \
222
209
  --timeout 300000 \
223
- --message '执行周治理:1) 验证 CURRENT_FOCUS.md 声称(PR 合并?文档存在?测试通过?),2) 更新 WEEK_STATE.json 指标,3) 记录到 WEEK_EVENTS.jsonl,4) 如果任务队列为空,从 OKR 推导任务并通知用户'
210
+ --message '执行周治理:1) 验证 CURRENT_FOCUS.md 声称(PR 合并?文档存在?测试通过?),2) 更新 WEEK_STATE.json 指标,3) 记录到 WEEK_EVENTS.jsonl,4) 如果任务队列为空,提醒用户规划下一阶段任务'
224
211
  ```
225
212
 
226
213
  **JSON 配置参考:**
@@ -46,7 +46,7 @@ disable-model-invocation: true
46
46
  ## Environment Capabilities
47
47
  Check @.state/SYSTEM_CAPABILITIES.json for high-performance tools (e.g., ripgrep, ast-grep) available in this environment. Use them!
48
48
  ```
49
- - 提示用户运行 `/manage-okr` 或 `/admin diagnose` 以让 Agent 感知新能力。
49
+ - 提示用户运行 `/pd-status` 或 `/admin diagnose` 以让 Agent 感知新能力。
50
50
  ## 核心原则
51
51
  - **喜新厌旧**: 敢于推荐新工具替代旧工具(如推荐 `pnpm` 替 `npm`,推荐 `vitest` 替 `jest`),但要说明理由。
52
52
  - **安全第一**: 在安装前必须获得用户明确授权。
@@ -51,4 +51,4 @@ disable-model-invocation: true
51
51
  ```
52
52
 
53
53
  ## 结项
54
- 完成写入后,提示用户:“✅ 战略锚点已锁定。建议运行 `/manage-okr` 进行季度/迭代级的任务拆解。”
54
+ 完成写入后,提示用户:”✅ 战略锚点已锁定。建议运行 `/pd-status` 查看系统状态,或使用 `/pd-bootstrap` 扫描环境工具。”
@@ -25,16 +25,13 @@ disable-model-invocation: true
25
25
 
26
26
  | 命令 | 用途 | 适用场景 |
27
27
  |------|------|----------|
28
- | `/pd-init` | 初始化战略与OKR | 新项目启动 |
29
- | `/pd-okr` | 目标与关键结果管理 | 周/月度复盘 |
28
+ | `/pd-init` | 初始化战略 | 新项目启动 |
30
29
  | `/pd-bootstrap` | 环境工具扫描与升级 | 装备升级 |
31
30
  | `/pd-research` | 发起工具升级研究 | 深度调研 |
32
31
  | `/pd-thinking` | 管理思维模型与候选方案 | 元认知管理 |
33
32
  | `/pd-evolve` | 执行完整进化循环 | 问题修复 |
34
- | `/pd-daily` | 配置并发送进化日报 | 日常查看 |
35
33
  | `/pd-evolution-status` | 查看EP等级与安全状态 | 状态查询 |
36
34
  | `/pd-status` | 查看系统状态(GFI和痛苦词典) | 健康检查 |
37
- | `/pd-grooming` | 工作区数字大扫除 | 熵减维护 |
38
35
  | `/pd-help` | 获取交互式命令引导 | 本技能 |
39
36
 
40
37
  ---
@@ -71,26 +68,14 @@ disable-model-invocation: true
71
68
  **触发条件**: 用户说"今天干了什么"、"看看进度"、"汇报一下"
72
69
 
73
70
  **推荐流程**:
74
- 1. `/pd-daily` - 发送今日进化日报
75
- 2. `/pd-evolution-status` - 查看当前信任积分
76
- 3. `/pd-okr` - 检查 OKR 对齐情况
71
+ 1. `/pd-evolution-status` - 查看当前信任积分
72
+ 2. `/pd-status` - 查看 GFI 和痛苦词典状态
77
73
 
78
- **话术**: "日报在手,进化我有。让我帮你回顾今天的成果。"
74
+ **话术**: "让我帮你回顾一下系统状态。"
79
75
 
80
76
  ---
81
77
 
82
- ### 场景 4: 工作区太乱
83
-
84
- **触发条件**: 用户说"项目太乱了"、"文件一堆"、"需要整理"
85
-
86
- **推荐流程**:
87
- 1. `/pd-grooming` - 启动工作区大扫除
88
-
89
- **话术**: "数字洁癖是一种美德。让我帮你熵减。"
90
-
91
- ---
92
-
93
- ### 场景 5: 权限或安全相关
78
+ ### 场景 4: 权限或安全相关
94
79
 
95
80
  **触发条件**: 用户说"权限不够"、"被拦截了"、"安全等级"
96
81
 
@@ -186,8 +171,8 @@ disable-model-invocation: true
186
171
  | 场景 | 组合流程 |
187
172
  |------|----------|
188
173
  | 大型重构 | `/pd-evolve` → `deductive-audit` → 执行 |
189
- | 系统优化 | `/pd-status` → `evolve-system` → `root-cause` |
190
- | 项目复盘 | `/pd-daily` → `/pd-okr` → `reflection-log` |
174
+ | 系统优化 | `/pd-status` → `root-cause` → 优化实施 |
175
+ | 项目复盘 | `/pd-evolution-status` → `/pd-status` → `reflection-log` |
191
176
 
192
177
  ### 内部技能调用
193
178
 
@@ -1,9 +1,9 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { handleInitStrategy, handleManageOkr } from '../../src/commands/strategy';
2
+ import { handleInitStrategy } from '../../src/commands/strategy';
3
3
 
4
4
  describe('Slash Commands Hook', () => {
5
5
  it('should handle /init-strategy command', () => {
6
- const mockCtx = {
6
+ const mockCtx = {
7
7
  workspaceDir: '/mock/workspace',
8
8
  commandBody: '/init-strategy',
9
9
  channel: 'cli',
@@ -16,19 +16,4 @@ describe('Slash Commands Hook', () => {
16
16
  expect(result).toBeDefined();
17
17
  expect(result.text).toContain('Strategy Initialization');
18
18
  });
19
-
20
- it('should handle /manage-okr command', () => {
21
- const mockCtx = {
22
- workspaceDir: '/mock/workspace',
23
- commandBody: '/manage-okr',
24
- channel: 'cli',
25
- isAuthorizedSender: true,
26
- config: {} as any
27
- };
28
-
29
- const result = handleManageOkr(mockCtx as any);
30
-
31
- expect(result).toBeDefined();
32
- expect(result.text).toContain('OKR Management');
33
- });
34
- });
19
+ });
@@ -0,0 +1,331 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { WorkspaceContext } from '../../src/core/workspace-context.js';
6
+
7
+ const mockLearner = {
8
+ getStore: vi.fn(() => ({ keywords: [{ term: 'wrong', weight: 0.5, hitCount: 3, truePositiveCount: 1, falsePositiveCount: 2 }] })),
9
+ };
10
+
11
+ const mockDb = {
12
+ listRecentSessions: vi.fn(() => [{ sessionId: 'session-1' }]),
13
+ listUserTurnsForSession: vi.fn(() => [{ rawExcerpt: 'User said wrong input', correctionDetected: true, correctionCue: 'wrong' }]),
14
+ };
15
+
16
+ const mockOptimizationService = {
17
+ buildTrajectoryHistory: vi.fn(async () => [
18
+ { sessionId: 'session-1', timestamp: 'now', term: 'wrong', userMessage: '' }
19
+ ]),
20
+ applyResult: vi.fn(),
21
+ };
22
+
23
+ vi.mock('../../src/core/correction-cue-learner.js', () => ({
24
+ CorrectionCueLearner: { get: vi.fn(() => mockLearner) },
25
+ }));
26
+
27
+ vi.mock('../../src/core/trajectory.js', () => ({
28
+ TrajectoryRegistry: {
29
+ get: vi.fn(() => mockDb),
30
+ clear: vi.fn(),
31
+ },
32
+ }));
33
+
34
+ vi.mock('../../src/service/keyword-optimization-service.js', () => ({
35
+ KeywordOptimizationService: { get: vi.fn(() => mockOptimizationService) },
36
+ }));
37
+
38
+ const mockDispatch = vi.fn().mockResolvedValue({
39
+ updated: true,
40
+ summary: 'Keyword store optimized',
41
+ updates: { wrong: { action: 'update', weight: 0.4, reasoning: 'slightly high FP' } }
42
+ });
43
+
44
+ const mockRegister = vi.fn();
45
+
46
+ vi.mock('@principles/core/runtime-v2', () => {
47
+ return {
48
+ WorkflowFunnelLoader: class {
49
+ getFunnel = vi.fn(() => ({
50
+ policy: {
51
+ runtimeKind: 'pi-ai',
52
+ provider: 'anthropic',
53
+ model: 'anthropic/claude-3-5-sonnet',
54
+ apiKeyEnv: 'ANTHROPIC_API_KEY',
55
+ timeoutMs: 30000,
56
+ }
57
+ }));
58
+ },
59
+ PiAiRuntimeAdapter: class {},
60
+ CorrectionObserver: class {},
61
+ AgentScheduler: class {
62
+ register = mockRegister;
63
+ dispatch = mockDispatch;
64
+ }
65
+ };
66
+ });
67
+
68
+ import { CorrectionObserverService, runCorrectionObserverCycle } from '../../src/service/correction-observer-service.js';
69
+ import { safeRmDir } from '../test-utils.js';
70
+
71
+ describe('CorrectionObserverService — Independent Service (PRI-293)', () => {
72
+ beforeEach(() => {
73
+ vi.useFakeTimers();
74
+ vi.clearAllMocks();
75
+ });
76
+
77
+ afterEach(() => {
78
+ vi.useRealTimers();
79
+ CorrectionObserverService.stop?.({} as any);
80
+ });
81
+
82
+ it('has correct service id', () => {
83
+ expect(CorrectionObserverService.id).toBe('principles-correction-observer');
84
+ });
85
+
86
+ it('starts and schedules periodic cycles', async () => {
87
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-obs-'));
88
+ const stateDir = path.join(workspaceDir, '.state');
89
+ fs.mkdirSync(stateDir, { recursive: true });
90
+
91
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
92
+
93
+ try {
94
+ CorrectionObserverService.start({
95
+ workspaceDir,
96
+ stateDir,
97
+ logger,
98
+ config: { get: () => undefined },
99
+ } as any);
100
+
101
+ expect(logger.info).toHaveBeenCalledWith(
102
+ expect.stringContaining('[PD:CorrectionObserver] Starting')
103
+ );
104
+
105
+ await vi.advanceTimersByTimeAsync(10_000);
106
+ for (let i = 0; i < 20; i++) {
107
+ await Promise.resolve();
108
+ }
109
+
110
+ expect(mockRegister).toHaveBeenCalled();
111
+ expect(mockDispatch).toHaveBeenCalledWith('correction-observer', expect.objectContaining({
112
+ parentSessionId: 'correction-observer-service',
113
+ workspaceDir,
114
+ recentMessages: ['User said wrong input'],
115
+ }));
116
+
117
+ expect(mockOptimizationService.applyResult).toHaveBeenCalledWith(expect.objectContaining({
118
+ updated: true,
119
+ summary: 'Keyword store optimized',
120
+ }));
121
+ } finally {
122
+ CorrectionObserverService.stop?.({} as any);
123
+ safeRmDir(workspaceDir);
124
+ }
125
+ });
126
+
127
+ it('stops cleanly and cancels pending timer', () => {
128
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-obs-stop-'));
129
+ const stateDir = path.join(workspaceDir, '.state');
130
+ fs.mkdirSync(stateDir, { recursive: true });
131
+
132
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
133
+
134
+ try {
135
+ CorrectionObserverService.start({
136
+ workspaceDir,
137
+ stateDir,
138
+ logger,
139
+ config: { get: () => undefined },
140
+ } as any);
141
+
142
+ CorrectionObserverService.stop?.({} as any);
143
+
144
+ vi.advanceTimersByTime(30_000);
145
+
146
+ expect(mockDispatch).not.toHaveBeenCalled();
147
+ } finally {
148
+ safeRmDir(workspaceDir);
149
+ }
150
+ });
151
+
152
+ it('does not reschedule after stop during active cycle (P2 fix)', async () => {
153
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-obs-race-'));
154
+ const stateDir = path.join(workspaceDir, '.state');
155
+ fs.mkdirSync(stateDir, { recursive: true });
156
+
157
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
158
+
159
+ let cycleResolve: () => void;
160
+ const cyclePromise = new Promise<void>(r => { cycleResolve = r; });
161
+ mockDispatch.mockImplementationOnce(async () => {
162
+ cycleResolve!();
163
+ return { updated: false, summary: 'in-flight' };
164
+ });
165
+
166
+ try {
167
+ CorrectionObserverService.start({
168
+ workspaceDir,
169
+ stateDir,
170
+ logger,
171
+ config: { get: () => undefined },
172
+ } as any);
173
+
174
+ await vi.advanceTimersByTimeAsync(10_000);
175
+ await cyclePromise;
176
+
177
+ CorrectionObserverService.stop?.({} as any);
178
+
179
+ vi.advanceTimersByTime(15 * 60 * 1000 * 2);
180
+
181
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
182
+ } finally {
183
+ CorrectionObserverService.stop?.({} as any);
184
+ safeRmDir(workspaceDir);
185
+ }
186
+ });
187
+
188
+ it('logs structured reason when workspaceDir is missing (ERR-002)', () => {
189
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
190
+
191
+ CorrectionObserverService.start({
192
+ workspaceDir: undefined as any,
193
+ logger,
194
+ config: { get: () => undefined },
195
+ } as any);
196
+
197
+ expect(logger.warn).toHaveBeenCalledWith(
198
+ expect.stringContaining('workspaceDir not found')
199
+ );
200
+ });
201
+
202
+ it('double start same workspace only dispatches one loop (P1 fix)', async () => {
203
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-dbl-'));
204
+ const stateDir = path.join(workspaceDir, '.state');
205
+ fs.mkdirSync(stateDir, { recursive: true });
206
+
207
+ const logger1 = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
208
+ const logger2 = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
209
+
210
+ try {
211
+ CorrectionObserverService.start({
212
+ workspaceDir,
213
+ stateDir,
214
+ logger: logger1,
215
+ config: { get: () => undefined },
216
+ } as any);
217
+
218
+ CorrectionObserverService.start({
219
+ workspaceDir,
220
+ stateDir,
221
+ logger: logger2,
222
+ config: { get: () => undefined },
223
+ } as any);
224
+
225
+ expect(logger2.info).toHaveBeenCalledWith(
226
+ expect.stringContaining('Already started')
227
+ );
228
+
229
+ await vi.advanceTimersByTimeAsync(10_000);
230
+ for (let i = 0; i < 20; i++) {
231
+ await Promise.resolve();
232
+ }
233
+
234
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
235
+ } finally {
236
+ CorrectionObserverService.stop?.({} as any);
237
+ safeRmDir(workspaceDir);
238
+ }
239
+ });
240
+
241
+ it('stop after double start cancels all timers and allows clean restart', async () => {
242
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-stopdbl-'));
243
+ const stateDir = path.join(workspaceDir, '.state');
244
+ fs.mkdirSync(stateDir, { recursive: true });
245
+
246
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
247
+
248
+ try {
249
+ CorrectionObserverService.start({
250
+ workspaceDir,
251
+ stateDir,
252
+ logger,
253
+ config: { get: () => undefined },
254
+ } as any);
255
+
256
+ CorrectionObserverService.start({
257
+ workspaceDir,
258
+ stateDir,
259
+ logger,
260
+ config: { get: () => undefined },
261
+ } as any);
262
+
263
+ CorrectionObserverService.stop?.({} as any);
264
+
265
+ vi.advanceTimersByTime(30_000);
266
+
267
+ expect(mockDispatch).not.toHaveBeenCalled();
268
+
269
+ CorrectionObserverService.start({
270
+ workspaceDir,
271
+ stateDir,
272
+ logger,
273
+ config: { get: () => undefined },
274
+ } as any);
275
+
276
+ expect(logger.info).toHaveBeenCalledWith(
277
+ expect.stringContaining('[PD:CorrectionObserver] Starting')
278
+ );
279
+ } finally {
280
+ CorrectionObserverService.stop?.({} as any);
281
+ safeRmDir(workspaceDir);
282
+ }
283
+ });
284
+ });
285
+
286
+ describe('runCorrectionObserverCycle — Independent Execution', () => {
287
+ it('skips cycle when no recent sessions exist', async () => {
288
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-cycle-'));
289
+ const stateDir = path.join(workspaceDir, '.state');
290
+ fs.mkdirSync(stateDir, { recursive: true });
291
+
292
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
293
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
294
+
295
+ mockDb.listRecentSessions.mockReturnValueOnce([]);
296
+
297
+ try {
298
+ await runCorrectionObserverCycle(wctx, logger as any);
299
+
300
+ expect(logger.info).toHaveBeenCalledWith(
301
+ expect.stringContaining('No recent sessions found')
302
+ );
303
+ expect(mockDispatch).not.toHaveBeenCalled();
304
+ } finally {
305
+ safeRmDir(workspaceDir);
306
+ }
307
+ });
308
+
309
+ it('logs structured error on cycle failure (ERR-002)', async () => {
310
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-err-'));
311
+ const stateDir = path.join(workspaceDir, '.state');
312
+ fs.mkdirSync(stateDir, { recursive: true });
313
+
314
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
315
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
316
+
317
+ mockDb.listRecentSessions.mockImplementationOnce(() => {
318
+ throw new Error('DB connection failed');
319
+ });
320
+
321
+ try {
322
+ await runCorrectionObserverCycle(wctx, logger as any);
323
+
324
+ expect(logger.warn).toHaveBeenCalledWith(
325
+ expect.stringContaining('Correction observer cycle failed')
326
+ );
327
+ } finally {
328
+ safeRmDir(workspaceDir);
329
+ }
330
+ });
331
+ });