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.
- package/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +250 -6
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +21 -47
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +110 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +319 -61
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +733 -0
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +156 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +130 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +503 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +77 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +77 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +89 -31
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
|
@@ -2,14 +2,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
EvolutionWorkerService,
|
|
4
4
|
createEvolutionTaskId,
|
|
5
|
-
createPainCandidateFingerprint,
|
|
6
5
|
extractEvolutionTaskId,
|
|
7
6
|
hasRecentDuplicateTask,
|
|
8
7
|
hasEquivalentPromotedRule,
|
|
9
|
-
processPromotion,
|
|
10
8
|
registerEvolutionTaskSession,
|
|
11
|
-
shouldTrackPainCandidate,
|
|
12
|
-
trackPainCandidate,
|
|
13
9
|
} from '../../src/service/evolution-worker.js';
|
|
14
10
|
import { DictionaryService } from '../../src/core/dictionary-service.js';
|
|
15
11
|
import * as sessionTracker from '../../src/core/session-tracker.js';
|
|
@@ -26,15 +22,6 @@ vi.mock('../../src/core/session-tracker', () => ({
|
|
|
26
22
|
flushAllSessions: vi.fn(),
|
|
27
23
|
listSessions: vi.fn(() => []), // Returns empty sessions for idle detection
|
|
28
24
|
}));
|
|
29
|
-
vi.mock('../../src/core/event-log', () => ({
|
|
30
|
-
EventLogService: {
|
|
31
|
-
get: vi.fn(() => ({
|
|
32
|
-
recordEvolutionTask: vi.fn(),
|
|
33
|
-
recordRulePromotion: vi.fn(),
|
|
34
|
-
flush: vi.fn(),
|
|
35
|
-
})),
|
|
36
|
-
},
|
|
37
|
-
}));
|
|
38
25
|
|
|
39
26
|
describe('EvolutionWorkerService', () => {
|
|
40
27
|
beforeEach(() => {
|
|
@@ -103,43 +90,6 @@ describe('EvolutionWorkerService', () => {
|
|
|
103
90
|
expect(idA).not.toBe(idB);
|
|
104
91
|
});
|
|
105
92
|
|
|
106
|
-
it('should generate distinct pain candidate fingerprints when only the suffix differs', () => {
|
|
107
|
-
const prefix = 'A'.repeat(60);
|
|
108
|
-
const textA = `${prefix} root-cause-one`;
|
|
109
|
-
const textB = `${prefix} root-cause-two`;
|
|
110
|
-
|
|
111
|
-
expect(createPainCandidateFingerprint(textA)).not.toBe(createPainCandidateFingerprint(textB));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should skip known noise payloads when tracking pain candidates', () => {
|
|
115
|
-
expect(shouldTrackPainCandidate('NO_REPLY')).toBe(false);
|
|
116
|
-
expect(shouldTrackPainCandidate(
|
|
117
|
-
'{"damageDetected":false,"severity":"mild","confidence":0.95,"reason":"observer"}'
|
|
118
|
-
)).toBe(false);
|
|
119
|
-
expect(shouldTrackPainCandidate('Tool edit failed on MEMORY.md')).toBe(true);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should initialize candidates as pending and keep longer samples', async () => {
|
|
123
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-candidate-'));
|
|
124
|
-
const candidatePath = path.join(dir, 'pain_candidates.json');
|
|
125
|
-
const longText = `Tool edit failed on MEMORY.md: ${'x'.repeat(1200)}`;
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
await trackPainCandidate(longText, {
|
|
129
|
-
resolve: () => candidatePath,
|
|
130
|
-
} as any);
|
|
131
|
-
|
|
132
|
-
const data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
|
|
133
|
-
const candidate = Object.values(data.candidates)[0] as any;
|
|
134
|
-
|
|
135
|
-
expect(candidate.status).toBe('pending');
|
|
136
|
-
expect(candidate.samples[0].length).toBeGreaterThan(200);
|
|
137
|
-
expect(candidate.samples[0].length).toBeLessThanOrEqual(1000);
|
|
138
|
-
} finally {
|
|
139
|
-
safeRmDir(dir);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
93
|
it('should extract evolution task ids from diagnostician payloads', () => {
|
|
144
94
|
expect(extractEvolutionTaskId('Diagnose systemic pain [ID: ab12cd34]. Source: tool_failure.')).toBe('ab12cd34');
|
|
145
95
|
expect(extractEvolutionTaskId('plain task without id')).toBeNull();
|
|
@@ -171,79 +121,6 @@ describe('EvolutionWorkerService', () => {
|
|
|
171
121
|
}
|
|
172
122
|
});
|
|
173
123
|
|
|
174
|
-
it('should promote legacy candidates even when status is missing', async () => {
|
|
175
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-promotion-'));
|
|
176
|
-
const candidatePath = path.join(dir, 'pain_candidates.json');
|
|
177
|
-
const addRule = vi.fn();
|
|
178
|
-
|
|
179
|
-
fs.writeFileSync(candidatePath, JSON.stringify({
|
|
180
|
-
candidates: {
|
|
181
|
-
deadbeef: {
|
|
182
|
-
count: 3,
|
|
183
|
-
firstSeen: '2026-03-20T00:00:00.000Z',
|
|
184
|
-
samples: [
|
|
185
|
-
'Tool edit failed on MEMORY.md. Could not find the exact text in MEMORY.md.',
|
|
186
|
-
'Tool edit failed on CURRENT_FOCUS.md. Could not find the exact text in CURRENT_FOCUS.md.',
|
|
187
|
-
'Tool edit failed on TEAM_COMMS.md. Could not find the exact text in TEAM_COMMS.md.',
|
|
188
|
-
],
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
}, null, 2), 'utf8');
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
await processPromotion({
|
|
195
|
-
workspaceDir: dir,
|
|
196
|
-
resolve: () => candidatePath,
|
|
197
|
-
config: {
|
|
198
|
-
get: (key: string) => {
|
|
199
|
-
if (key === 'thresholds.promotion_count_threshold') return 3;
|
|
200
|
-
if (key === 'scores.default_confusion') return 35;
|
|
201
|
-
return undefined;
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
dictionary: {
|
|
205
|
-
addRule,
|
|
206
|
-
getAllRules: () => ({}),
|
|
207
|
-
},
|
|
208
|
-
} as any, { info: vi.fn() }, null);
|
|
209
|
-
|
|
210
|
-
const saved = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
|
|
211
|
-
expect(addRule).toHaveBeenCalled();
|
|
212
|
-
expect(saved.candidates.deadbeef.status).toBe('promoted');
|
|
213
|
-
} finally {
|
|
214
|
-
safeRmDir(dir);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should flush the dictionary on its interval', async () => {
|
|
219
|
-
const mockDict = {
|
|
220
|
-
flush: vi.fn()
|
|
221
|
-
};
|
|
222
|
-
vi.mocked(DictionaryService.get).mockReturnValue(mockDict as any);
|
|
223
|
-
|
|
224
|
-
// Use path.resolve for cross-platform compatibility
|
|
225
|
-
const workspaceDir = path.resolve('/mock/workspace');
|
|
226
|
-
const expectedStateDir = path.join(workspaceDir, '.state');
|
|
227
|
-
|
|
228
|
-
const ctx = {
|
|
229
|
-
workspaceDir,
|
|
230
|
-
stateDir: path.resolve('/mock/state'),
|
|
231
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
EvolutionWorkerService.start(ctx as any);
|
|
235
|
-
|
|
236
|
-
// Advance by 15 minutes
|
|
237
|
-
await vi.advanceTimersByTimeAsync(15 * 60 * 1000);
|
|
238
|
-
|
|
239
|
-
expect(mockDict.flush).toHaveBeenCalled();
|
|
240
|
-
// Service now uses workspace-specific stateDir, not ctx.stateDir
|
|
241
|
-
expect(sessionTracker.initPersistence).toHaveBeenCalledWith(expectedStateDir);
|
|
242
|
-
expect(sessionTracker.flushAllSessions).toHaveBeenCalled();
|
|
243
|
-
|
|
244
|
-
EvolutionWorkerService.stop(ctx as any);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
124
|
it('should process queue work without persisting a legacy directive file', async () => {
|
|
248
125
|
const mockDict = {
|
|
249
126
|
flush: vi.fn()
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { executeNocturnalReflection } from '../../src/service/nocturnal-service.js';
|
|
6
|
+
import { createNocturnalTrajectoryExtractor } from '../../src/core/nocturnal-trajectory-extractor.js';
|
|
7
|
+
import { getDatasetRecordByArtifactId, listDatasetRecords } from '../../src/core/nocturnal-dataset.js';
|
|
8
|
+
import { listArtifactLineageRecords } from '../../src/core/nocturnal-artifact-lineage.js';
|
|
9
|
+
import {
|
|
10
|
+
listImplementationsForRule,
|
|
11
|
+
saveLedger,
|
|
12
|
+
type LedgerPrinciple,
|
|
13
|
+
type LedgerRule,
|
|
14
|
+
} from '../../src/core/principle-tree-ledger.js';
|
|
15
|
+
import {
|
|
16
|
+
getImplementationAssetRoot,
|
|
17
|
+
loadEntrySource,
|
|
18
|
+
loadManifest,
|
|
19
|
+
} from '../../src/core/code-implementation-storage.js';
|
|
20
|
+
import { TrajectoryDatabase, TrajectoryRegistry } from '../../src/core/trajectory.js';
|
|
21
|
+
import { clearSession, listSessions, seedSessionForTest } from '../../src/core/session-tracker.js';
|
|
22
|
+
import { safeRmDir } from '../test-utils.js';
|
|
23
|
+
|
|
24
|
+
function makePrinciple(overrides: Partial<LedgerPrinciple> = {}): LedgerPrinciple {
|
|
25
|
+
return {
|
|
26
|
+
id: 'T-08',
|
|
27
|
+
version: 1,
|
|
28
|
+
text: 'Pain as Signal',
|
|
29
|
+
triggerPattern: 'pain',
|
|
30
|
+
action: 'Diagnose before repeating failures',
|
|
31
|
+
status: 'active',
|
|
32
|
+
priority: 'P1',
|
|
33
|
+
scope: 'general',
|
|
34
|
+
evaluability: 'deterministic',
|
|
35
|
+
valueScore: 0,
|
|
36
|
+
adherenceRate: 80,
|
|
37
|
+
painPreventedCount: 0,
|
|
38
|
+
derivedFromPainIds: [],
|
|
39
|
+
ruleIds: ['R-001'],
|
|
40
|
+
conflictsWithPrincipleIds: [],
|
|
41
|
+
createdAt: '2026-04-08T00:00:00.000Z',
|
|
42
|
+
updatedAt: '2026-04-08T00:00:00.000Z',
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeRule(overrides: Partial<LedgerRule> = {}): LedgerRule {
|
|
48
|
+
return {
|
|
49
|
+
id: 'R-001',
|
|
50
|
+
version: 1,
|
|
51
|
+
name: 'Protect risky write',
|
|
52
|
+
description: 'Require approval for risky write operations.',
|
|
53
|
+
type: 'gate',
|
|
54
|
+
triggerCondition: 'risky write',
|
|
55
|
+
enforcement: 'block',
|
|
56
|
+
action: 'require approval for risky write',
|
|
57
|
+
principleId: 'T-08',
|
|
58
|
+
status: 'implemented',
|
|
59
|
+
coverageRate: 0,
|
|
60
|
+
falsePositiveRate: 0,
|
|
61
|
+
implementationIds: [],
|
|
62
|
+
createdAt: '2026-04-08T00:00:00.000Z',
|
|
63
|
+
updatedAt: '2026-04-08T00:00:00.000Z',
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function makeBehavioralArtifact(sessionId: string) {
|
|
69
|
+
return {
|
|
70
|
+
artifactId:
|
|
71
|
+
sessionId === 'session-skip'
|
|
72
|
+
? '11111111-1111-4111-8111-111111111111'
|
|
73
|
+
: sessionId === 'session-success'
|
|
74
|
+
? '22222222-2222-4222-8222-222222222222'
|
|
75
|
+
: sessionId === 'session-invalid'
|
|
76
|
+
? '33333333-3333-4333-8333-333333333333'
|
|
77
|
+
: '44444444-4444-4444-8444-444444444444',
|
|
78
|
+
sessionId,
|
|
79
|
+
principleId: 'T-08',
|
|
80
|
+
sourceSnapshotRef: `snapshot-${sessionId}`,
|
|
81
|
+
badDecision: 'Retried a failing write operation without first checking the approval requirements',
|
|
82
|
+
betterDecision:
|
|
83
|
+
'Read docs/gateblocks.md to confirm the approval requirements before retrying the write operation',
|
|
84
|
+
rationale: 'Checking the gate guidance first prevents repeated blocked writes and preserves operator review boundaries',
|
|
85
|
+
createdAt: '2026-04-08T00:00:00.000Z',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('nocturnal-service code candidates', () => {
|
|
90
|
+
let tempDir: string;
|
|
91
|
+
let workspaceDir: string;
|
|
92
|
+
let stateDir: string;
|
|
93
|
+
let trajectory: TrajectoryDatabase;
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-code-candidate-'));
|
|
97
|
+
workspaceDir = path.join(tempDir, 'workspace');
|
|
98
|
+
stateDir = path.join(tempDir, 'state');
|
|
99
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
100
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
101
|
+
trajectory = new TrajectoryDatabase({ workspaceDir });
|
|
102
|
+
createNocturnalTrajectoryExtractor(workspaceDir);
|
|
103
|
+
|
|
104
|
+
for (const session of listSessions()) {
|
|
105
|
+
clearSession(session.sessionId);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
try {
|
|
111
|
+
trajectory.dispose();
|
|
112
|
+
} catch {
|
|
113
|
+
// Best effort cleanup.
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
TrajectoryRegistry.dispose(workspaceDir);
|
|
117
|
+
} catch {
|
|
118
|
+
// Best effort cleanup.
|
|
119
|
+
}
|
|
120
|
+
safeRmDir(tempDir);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function makeIdleResult() {
|
|
124
|
+
return {
|
|
125
|
+
isIdle: true,
|
|
126
|
+
mostRecentActivityAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
127
|
+
idleForMs: 2 * 60 * 60 * 1000,
|
|
128
|
+
userActiveSessions: 0,
|
|
129
|
+
abandonedSessionIds: [],
|
|
130
|
+
trajectoryGuardrailConfirmsIdle: true,
|
|
131
|
+
reason: 'test override',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function seedSession(sessionId: string): void {
|
|
136
|
+
const startedAt = new Date().toISOString();
|
|
137
|
+
trajectory.recordSession({ sessionId, startedAt });
|
|
138
|
+
for (let i = 0; i < 2; i += 1) {
|
|
139
|
+
trajectory.recordToolCall({
|
|
140
|
+
sessionId,
|
|
141
|
+
toolName: 'write',
|
|
142
|
+
outcome: 'failure',
|
|
143
|
+
errorMessage: 'risky write blocked',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
trajectory.recordPainEvent({
|
|
147
|
+
sessionId,
|
|
148
|
+
source: 'gate',
|
|
149
|
+
score: 75,
|
|
150
|
+
reason: 'risky write requires approval',
|
|
151
|
+
});
|
|
152
|
+
trajectory.recordGateBlock({
|
|
153
|
+
sessionId,
|
|
154
|
+
toolName: 'write',
|
|
155
|
+
reason: 'risky write requires approval',
|
|
156
|
+
riskLevel: 'medium',
|
|
157
|
+
});
|
|
158
|
+
seedSessionForTest(sessionId, workspaceDir, new Date(startedAt).getTime());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function seedLedger(singleRule = true): void {
|
|
162
|
+
saveLedger(stateDir, {
|
|
163
|
+
trainingStore: {
|
|
164
|
+
'T-08': {
|
|
165
|
+
principleId: 'T-08',
|
|
166
|
+
evaluability: 'deterministic',
|
|
167
|
+
applicableOpportunityCount: 5,
|
|
168
|
+
observedViolationCount: 3,
|
|
169
|
+
complianceRate: 0.2,
|
|
170
|
+
violationTrend: 1,
|
|
171
|
+
generatedSampleCount: 0,
|
|
172
|
+
approvedSampleCount: 0,
|
|
173
|
+
includedTrainRunIds: [],
|
|
174
|
+
deployedCheckpointIds: [],
|
|
175
|
+
internalizationStatus: 'internalized',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
tree: {
|
|
179
|
+
principles: {
|
|
180
|
+
'T-08': makePrinciple({
|
|
181
|
+
ruleIds: singleRule ? ['R-001'] : ['R-001', 'R-002'],
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
rules: singleRule
|
|
185
|
+
? {
|
|
186
|
+
'R-001': makeRule(),
|
|
187
|
+
}
|
|
188
|
+
: {
|
|
189
|
+
'R-001': makeRule(),
|
|
190
|
+
'R-002': makeRule({
|
|
191
|
+
id: 'R-002',
|
|
192
|
+
name: 'Protect risky write',
|
|
193
|
+
description: 'Require approval for risky write operations.',
|
|
194
|
+
triggerCondition: 'risky write',
|
|
195
|
+
action: 'require approval for risky write',
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
implementations: {},
|
|
199
|
+
metrics: {},
|
|
200
|
+
lastUpdated: '2026-04-08T00:00:00.000Z',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
it('persists a behavioral artifact only when Artificer routing is skipped', () => {
|
|
206
|
+
seedLedger(false);
|
|
207
|
+
seedSession('session-skip');
|
|
208
|
+
|
|
209
|
+
const result = executeNocturnalReflection(workspaceDir, stateDir, {
|
|
210
|
+
idleCheckOverride: makeIdleResult(),
|
|
211
|
+
skipReflector: true,
|
|
212
|
+
reflectorOutputOverride: JSON.stringify(makeBehavioralArtifact('session-skip')),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(result.success).toBe(true);
|
|
216
|
+
expect(result.diagnostics.persisted).toBe(true);
|
|
217
|
+
expect(result.diagnostics.artificer.status).toBe('skipped');
|
|
218
|
+
expect(result.diagnostics.artificer.reason).toBe('no_deterministic_rule');
|
|
219
|
+
expect(listImplementationsForRule(stateDir, 'R-001')).toHaveLength(0);
|
|
220
|
+
|
|
221
|
+
const datasetRecord = getDatasetRecordByArtifactId(
|
|
222
|
+
workspaceDir,
|
|
223
|
+
result.artifact!.artifactId
|
|
224
|
+
);
|
|
225
|
+
expect(datasetRecord?.classification).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('persists a behavioral artifact and a candidate implementation when Artificer succeeds', () => {
|
|
229
|
+
seedLedger(true);
|
|
230
|
+
seedSession('session-success');
|
|
231
|
+
|
|
232
|
+
const result = executeNocturnalReflection(workspaceDir, stateDir, {
|
|
233
|
+
idleCheckOverride: makeIdleResult(),
|
|
234
|
+
skipReflector: true,
|
|
235
|
+
reflectorOutputOverride: JSON.stringify(makeBehavioralArtifact('session-success')),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(result.success).toBe(true);
|
|
239
|
+
expect(result.diagnostics.artificer.status).toBe('persisted_candidate');
|
|
240
|
+
|
|
241
|
+
const implementations = listImplementationsForRule(stateDir, 'R-001');
|
|
242
|
+
expect(implementations).toHaveLength(1);
|
|
243
|
+
expect(implementations[0].lifecycleState).toBe('candidate');
|
|
244
|
+
|
|
245
|
+
const manifest = loadManifest(stateDir, implementations[0].id);
|
|
246
|
+
expect(manifest?.lineage).toMatchObject({
|
|
247
|
+
principleId: 'T-08',
|
|
248
|
+
ruleId: 'R-001',
|
|
249
|
+
sourceSessionId: 'session-success',
|
|
250
|
+
});
|
|
251
|
+
expect(loadEntrySource(stateDir, implementations[0].id)).toContain(
|
|
252
|
+
'requireApproval'
|
|
253
|
+
);
|
|
254
|
+
expect(fs.existsSync(getImplementationAssetRoot(stateDir, implementations[0].id))).toBe(
|
|
255
|
+
true
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const lineage = listArtifactLineageRecords(workspaceDir);
|
|
259
|
+
expect(
|
|
260
|
+
lineage.some((record) => record.artifactKind === 'behavioral-sample')
|
|
261
|
+
).toBe(true);
|
|
262
|
+
expect(
|
|
263
|
+
lineage.some(
|
|
264
|
+
(record) =>
|
|
265
|
+
record.artifactKind === 'rule-implementation-candidate' &&
|
|
266
|
+
record.implementationId === implementations[0].id
|
|
267
|
+
)
|
|
268
|
+
).toBe(true);
|
|
269
|
+
|
|
270
|
+
const datasetRecord = getDatasetRecordByArtifactId(
|
|
271
|
+
workspaceDir,
|
|
272
|
+
result.artifact!.artifactId
|
|
273
|
+
);
|
|
274
|
+
expect(datasetRecord?.classification).toBeNull();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('preserves the behavioral artifact when Artificer validation fails', () => {
|
|
278
|
+
seedLedger(true);
|
|
279
|
+
seedSession('session-invalid');
|
|
280
|
+
|
|
281
|
+
const result = executeNocturnalReflection(workspaceDir, stateDir, {
|
|
282
|
+
idleCheckOverride: makeIdleResult(),
|
|
283
|
+
skipReflector: true,
|
|
284
|
+
reflectorOutputOverride: JSON.stringify(makeBehavioralArtifact('session-invalid')),
|
|
285
|
+
artificerOutputOverride: JSON.stringify({
|
|
286
|
+
ruleId: 'R-001',
|
|
287
|
+
candidateSource:
|
|
288
|
+
'export const meta = { name: "bad", version: "1", ruleId: "R-001", coversCondition: "bad" }; export function evaluate() { eval("1 + 1"); return { decision: "allow", matched: false, reason: "bad" }; }',
|
|
289
|
+
helperUsage: [],
|
|
290
|
+
expectedDecision: 'allow',
|
|
291
|
+
rationale: 'bad candidate',
|
|
292
|
+
lineage: {
|
|
293
|
+
artifactKind: 'rule-implementation-candidate',
|
|
294
|
+
sourceSnapshotRef: 'snapshot-1',
|
|
295
|
+
sourcePainIds: ['pain:1'],
|
|
296
|
+
sourceGateBlockIds: ['gate:1'],
|
|
297
|
+
},
|
|
298
|
+
}),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(result.success).toBe(true);
|
|
302
|
+
expect(result.diagnostics.persisted).toBe(true);
|
|
303
|
+
expect(result.diagnostics.artificer.status).toBe('validation_failed');
|
|
304
|
+
expect(result.diagnostics.artificer.reason).toBe('validator_rejected');
|
|
305
|
+
expect(listImplementationsForRule(stateDir, 'R-001')).toHaveLength(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('cleans up the candidate when storage fails after ledger creation', () => {
|
|
309
|
+
seedLedger(true);
|
|
310
|
+
seedSession('session-storage-failure');
|
|
311
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
312
|
+
fs.writeFileSync(path.join(stateDir, 'principles'), 'not-a-directory', 'utf-8');
|
|
313
|
+
|
|
314
|
+
const result = executeNocturnalReflection(workspaceDir, stateDir, {
|
|
315
|
+
idleCheckOverride: makeIdleResult(),
|
|
316
|
+
skipReflector: true,
|
|
317
|
+
reflectorOutputOverride: JSON.stringify(
|
|
318
|
+
makeBehavioralArtifact('session-storage-failure')
|
|
319
|
+
),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(result.success).toBe(true);
|
|
323
|
+
expect(result.diagnostics.artificer.status).toBe('validation_failed');
|
|
324
|
+
expect(result.diagnostics.artificer.reason).toBe('persistence_failed');
|
|
325
|
+
expect(listImplementationsForRule(stateDir, 'R-001')).toHaveLength(0);
|
|
326
|
+
expect(
|
|
327
|
+
listArtifactLineageRecords(workspaceDir, 'rule-implementation-candidate')
|
|
328
|
+
).toHaveLength(0);
|
|
329
|
+
});
|
|
330
|
+
});
|
package/tests/utils/nlp.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { extractCommonPhrases
|
|
2
|
+
import { extractCommonPhrases } from '../../src/utils/nlp.js';
|
|
3
3
|
|
|
4
4
|
describe('NLP Utils', () => {
|
|
5
5
|
describe('extractCommonPhrases (English)', () => {
|
|
@@ -32,22 +32,4 @@ describe('NLP Utils', () => {
|
|
|
32
32
|
expect(result).toHaveLength(0);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
-
|
|
36
|
-
describe('extractCommonSubstring (Chinese/Mixed)', () => {
|
|
37
|
-
it('should extract common substring from Chinese text', () => {
|
|
38
|
-
const samples = [
|
|
39
|
-
"这个问题让我有点头疼,不知道怎么解决。",
|
|
40
|
-
"代码报错了,让我有点头疼。",
|
|
41
|
-
"让我有点头疼,这逻辑不对啊。"
|
|
42
|
-
];
|
|
43
|
-
const result = extractCommonSubstring(samples, 4);
|
|
44
|
-
// "让我有点头疼" is 6 chars, so it should find it or a part of it
|
|
45
|
-
expect(result[0]).toContain('让我有点头疼');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should handle short or insufficient samples', () => {
|
|
49
|
-
expect(extractCommonSubstring(["仅有一个样本"])).toHaveLength(0);
|
|
50
|
-
expect(extractCommonSubstring([], 4)).toHaveLength(0);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
35
|
});
|