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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollback Implementation Command — CLI `/pd-rollback-impl`
|
|
3
|
+
* ==========================================================
|
|
4
|
+
*
|
|
5
|
+
* PURPOSE: Revert the current active implementation and restore the previous one.
|
|
6
|
+
*
|
|
7
|
+
* FLOW:
|
|
8
|
+
* 1. List active implementations
|
|
9
|
+
* 2. On confirm: current -> disabled, previous -> active
|
|
10
|
+
*
|
|
11
|
+
* SAFETY:
|
|
12
|
+
* - If no previous active: current -> disabled (degrades to hard-boundary gates)
|
|
13
|
+
* - Rollback records persisted with full audit trail
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { withLock } from '../utils/file-lock.js';
|
|
19
|
+
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
20
|
+
import { refreshPrincipleLifecycle } from '../core/principle-internalization/lifecycle-refresh.js';
|
|
21
|
+
import {
|
|
22
|
+
loadLedger,
|
|
23
|
+
transitionImplementationState,
|
|
24
|
+
} from '../core/principle-tree-ledger.js';
|
|
25
|
+
import type { Implementation } from '../types/principle-tree-schema.js';
|
|
26
|
+
import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get all implementations from the ledger.
|
|
30
|
+
*/
|
|
31
|
+
function getAllImplementations(stateDir: string): Implementation[] {
|
|
32
|
+
const ledger = loadLedger(stateDir);
|
|
33
|
+
return Object.values(ledger.tree.implementations);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handle the /pd-rollback-impl command.
|
|
38
|
+
*
|
|
39
|
+
* Usage:
|
|
40
|
+
* /pd-rollback-impl list - List active implementations
|
|
41
|
+
* /pd-rollback-impl <implId> - Rollback current active
|
|
42
|
+
* /pd-rollback-impl <implId> --reason "<reason>" - Rollback with reason
|
|
43
|
+
*/
|
|
44
|
+
export function handleRollbackImplCommand(ctx: PluginCommandContext): PluginCommandResult {
|
|
45
|
+
const workspaceDir = (ctx.config?.workspaceDir as string) || process.cwd();
|
|
46
|
+
const {stateDir} = WorkspaceContext.fromHookContext({ ...ctx, workspaceDir });
|
|
47
|
+
const lang = (ctx.config?.language as string) || 'en';
|
|
48
|
+
const isZh = lang === 'zh';
|
|
49
|
+
|
|
50
|
+
const args = (ctx.args || '').trim();
|
|
51
|
+
|
|
52
|
+
// Parse args
|
|
53
|
+
const subcommand = args.split(/\s+/)[0] || '';
|
|
54
|
+
const implId = subcommand === 'list' ? '' : subcommand;
|
|
55
|
+
const reasonMatch = (/--reason\s+"([^"]+)"/.exec(args)) || (/--reason\s+(\S+)/.exec(args));
|
|
56
|
+
const reason = reasonMatch ? reasonMatch[1] : null;
|
|
57
|
+
|
|
58
|
+
// List active
|
|
59
|
+
if (subcommand === 'list' || subcommand === '') {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: Mutual recursion between helper functions - reordering would break logical grouping
|
|
61
|
+
return _handleListActiveRollback(stateDir, isZh);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: Mutual recursion between helper functions - reordering would break logical grouping
|
|
65
|
+
return _handleRollbackImpl(workspaceDir, stateDir, implId, reason, isZh, ctx.sessionId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _handleListActiveRollback(
|
|
69
|
+
stateDir: string,
|
|
70
|
+
isZh: boolean
|
|
71
|
+
): PluginCommandResult {
|
|
72
|
+
const allImpls = getAllImplementations(stateDir);
|
|
73
|
+
const activeImpls = allImpls.filter(
|
|
74
|
+
(impl) => impl.lifecycleState === 'active'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (activeImpls.length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
text: isZh
|
|
80
|
+
? '\n\u2139\ufe0f \u6ca1\u6709\u6d3b\u8dc3\u5b9e\u73b0\u53ef\u56de\u6eda\u3002'
|
|
81
|
+
: '\n\u2139\ufe0f No active implementations to rollback.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let output = isZh ? '\n\ud83d\udccb \u53ef\u56de\u6eda\u6d3b\u8dc3\u5b9e\u73b0\n' : '\n\ud83d\udccb Active Implementations Available for Rollback\n';
|
|
86
|
+
output += `${'='.repeat(50)}\n`;
|
|
87
|
+
|
|
88
|
+
for (const impl of activeImpls) {
|
|
89
|
+
const hasPrevious = impl.previousActive ? '\u2705' : '\u26a0\ufe0f \u65e0\u524d\u4e00\u4e2a';
|
|
90
|
+
output += ` ${impl.id}\n`;
|
|
91
|
+
output += ` Rule: ${impl.ruleId} | Version: ${impl.version}\n`;
|
|
92
|
+
output += ` Has previous: ${hasPrevious}\n`;
|
|
93
|
+
if (impl.previousActive) {
|
|
94
|
+
output += ` Previous: ${impl.previousActive}\n`;
|
|
95
|
+
}
|
|
96
|
+
output += '\n';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
output += isZh
|
|
100
|
+
? '\u7528\u6cd5: /pd-rollback-impl <implId> --reason "\u539f\u56e0"'
|
|
101
|
+
: 'Usage: /pd-rollback-impl <implId> --reason "reason"';
|
|
102
|
+
|
|
103
|
+
return { text: output };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: Command handler signature must match OpenClaw plugin interface - breaking API change to options objects would affect public contracts
|
|
107
|
+
function _handleRollbackImpl(
|
|
108
|
+
workspaceDir: string,
|
|
109
|
+
stateDir: string,
|
|
110
|
+
implId: string,
|
|
111
|
+
reason: string | null,
|
|
112
|
+
isZh: boolean,
|
|
113
|
+
sessionId?: string,
|
|
114
|
+
): PluginCommandResult {
|
|
115
|
+
const allImpls = getAllImplementations(stateDir);
|
|
116
|
+
const currentActive = allImpls.find((i) => i.id === implId);
|
|
117
|
+
|
|
118
|
+
if (!currentActive) {
|
|
119
|
+
return {
|
|
120
|
+
text: isZh
|
|
121
|
+
? `\u274c \u672a\u627e\u5230\u5b9e\u73b0: ${implId}`
|
|
122
|
+
: `\u274c Implementation not found: ${implId}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const currentState = currentActive.lifecycleState;
|
|
127
|
+
if (currentState !== 'active') {
|
|
128
|
+
return {
|
|
129
|
+
text: isZh
|
|
130
|
+
? `\u274c \u5b9e\u73b0 ${implId} \u5f53\u524d\u72b6\u6001: ${currentState}\n\u53ea\u80fd\u56de\u6eda\u6d3b\u8dc3\u5b9e\u73b0\u3002`
|
|
131
|
+
: `\u274c Implementation ${implId} is in state: ${currentState}\nCan only rollback active implementations.`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const previousActiveId = currentActive.previousActive;
|
|
136
|
+
const reasonText = reason || (isZh ? '\u7528\u6237\u624b\u52a8\u56de\u6eda' : 'User manual rollback');
|
|
137
|
+
|
|
138
|
+
// Step 1: Current active -> disabled
|
|
139
|
+
transitionImplementationState(stateDir, implId, 'disabled');
|
|
140
|
+
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned immediately in if/else branches before use
|
|
142
|
+
let restoredMessage: string;
|
|
143
|
+
|
|
144
|
+
if (previousActiveId && allImpls.some((i) => i.id === previousActiveId)) {
|
|
145
|
+
// Step 2: Restore previous active -> active
|
|
146
|
+
transitionImplementationState(stateDir, previousActiveId, 'active');
|
|
147
|
+
|
|
148
|
+
restoredMessage = isZh
|
|
149
|
+
? `\n \u5df2\u6062\u590d\u524d\u4e00\u4e2a\u6d3b\u8dc3\u5b9e\u73b0: ${previousActiveId}\n \u72b6\u6001: disabled -> active`
|
|
150
|
+
: `\n Restored previous active implementation: ${previousActiveId}\n State: disabled -> active`;
|
|
151
|
+
} else {
|
|
152
|
+
// No previous active — degrade to hard-boundary gates (per Phase 12 D-08)
|
|
153
|
+
restoredMessage = isZh
|
|
154
|
+
? `\n \u26a0\ufe0f \u6ca1\u6709\u524d\u4e00\u4e2a\u6d3b\u8dc3\u5b9e\u73b0\uff0c\u8be5\u89c4\u5219\u6ca1\u6709\u6d3b\u8dc3\u5b9e\u73b0\u3002`
|
|
155
|
+
+ `\n GFI\u3001Progressive Gate\u7b49\u786c\u56fa\u62b1\u95e8\u4ecd\u7136\u6b63\u5e38\u8fd0\u884c\u3002`
|
|
156
|
+
: `\n \u26a0\ufe0f No previous active implementation. Rule has no active code implementation.`
|
|
157
|
+
+ ` Existing hard-boundary gates (GFI, Progressive Gate) continue functioning normally.`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Store rollback record
|
|
161
|
+
const rollbackDir = path.join(
|
|
162
|
+
stateDir,
|
|
163
|
+
'principles',
|
|
164
|
+
'implementations',
|
|
165
|
+
implId,
|
|
166
|
+
'rollbacks'
|
|
167
|
+
);
|
|
168
|
+
if (!fs.existsSync(rollbackDir)) {
|
|
169
|
+
fs.mkdirSync(rollbackDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const rollbackRecord = {
|
|
173
|
+
rolledBackBy: sessionId || 'manual',
|
|
174
|
+
rolledBackAt: new Date().toISOString(),
|
|
175
|
+
reason: reasonText,
|
|
176
|
+
previousImplementationId: previousActiveId || null,
|
|
177
|
+
restoredImplementationId: previousActiveId && allImpls.some((i) => i.id === previousActiveId)
|
|
178
|
+
? previousActiveId
|
|
179
|
+
: null,
|
|
180
|
+
rolledBackImplId: implId,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const rollbackTimestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
184
|
+
const rollbackPath = path.join(rollbackDir, `${rollbackTimestamp}.json`);
|
|
185
|
+
withLock(rollbackPath, () => {
|
|
186
|
+
fs.writeFileSync(rollbackPath, JSON.stringify(rollbackRecord, null, 2), 'utf-8');
|
|
187
|
+
});
|
|
188
|
+
refreshPrincipleLifecycle(workspaceDir, stateDir);
|
|
189
|
+
|
|
190
|
+
let output = isZh
|
|
191
|
+
? `\n\u2705 \u56de\u6eda\u5b8c\u6210: ${implId}\n \u72b6\u6001: active -> disabled\n \u539f\u56e0: ${reasonText}`
|
|
192
|
+
: `\n\u2705 Rollback complete: ${implId}\n State: active -> disabled\n Reason: ${reasonText}`;
|
|
193
|
+
|
|
194
|
+
output += restoredMessage;
|
|
195
|
+
|
|
196
|
+
return { text: output };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Handle natural language rollback request.
|
|
201
|
+
* Detects phrases like "回滚这个规则实现", "rollback this implementation".
|
|
202
|
+
*/
|
|
203
|
+
export function handleNaturalLanguageRollbackImpl(
|
|
204
|
+
workspaceDir: string,
|
|
205
|
+
sessionId: string | undefined,
|
|
206
|
+
reason: string
|
|
207
|
+
): { success: boolean; message: string } {
|
|
208
|
+
const isZh = (/[\u4e00-\u9fff]/.exec(reason)) || false;
|
|
209
|
+
const {stateDir} = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
210
|
+
|
|
211
|
+
if (!sessionId) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
message: isZh ? '\u65e0\u6cd5\u8bc6\u522b\u5f53\u524d\u4f1a\u8bdd' : 'Session not found',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Natural language entry: get last active implementation
|
|
219
|
+
const allImpls = getAllImplementations(stateDir);
|
|
220
|
+
const lastActive = allImpls.find((i) => i.lifecycleState === 'active');
|
|
221
|
+
|
|
222
|
+
if (!lastActive) {
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
message: isZh ? '\u5f53\u524d\u6ca1\u6709\u6d3b\u8dc3\u5b9e\u73b0' : 'No active implementations',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Would be called via the hook — return the impl ID for the hook to route
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
message: `${lastActive.id}|${reason}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
package/src/commands/rollback.ts
CHANGED
|
@@ -15,7 +15,8 @@ export function handleRollbackCommand(ctx: PluginCommandContext): PluginCommandR
|
|
|
15
15
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
|
|
16
16
|
const lang = (ctx.config?.language as string) || 'en';
|
|
17
17
|
const isZh = lang === 'zh';
|
|
18
|
-
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: sessionId injected by OpenClaw plugin framework - type not available in PluginCommandContext
|
|
19
|
+
const {sessionId} = (ctx as any);
|
|
19
20
|
|
|
20
21
|
const args = (ctx.args || '').trim();
|
|
21
22
|
|
|
@@ -43,8 +44,10 @@ Usage:
|
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
let
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned immediately in if/else branches before use
|
|
48
|
+
let eventId: string | null;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: triggerMethod is reserved for future extension - tracking rollback trigger source
|
|
50
|
+
const _triggerMethod = 'user_command' as const;
|
|
48
51
|
|
|
49
52
|
if (args === 'last') {
|
|
50
53
|
// Find the last empathy event in current session
|
package/src/commands/samples.ts
CHANGED
|
@@ -29,9 +29,11 @@ export function handleSamplesCommand(ctx: PluginCommandContext): PluginCommandRe
|
|
|
29
29
|
}
|
|
30
30
|
const normalizedDecision = decision === 'approve' ? 'approved' : 'rejected';
|
|
31
31
|
const note = noteParts.join(' ').trim();
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try block, catch has early return
|
|
32
33
|
let record;
|
|
33
34
|
try {
|
|
34
35
|
record = wctx.trajectory.reviewCorrectionSample(sampleId, normalizedDecision, note);
|
|
36
|
+
/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: error handling only - returning failure response */
|
|
35
37
|
} catch (error) {
|
|
36
38
|
return {
|
|
37
39
|
text: zh
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
2
|
import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
|
|
4
3
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
5
4
|
|
|
@@ -16,7 +15,7 @@ function getModels(wctx: WorkspaceContext): Record<string, string> {
|
|
|
16
15
|
const content = fs.readFileSync(modelsPath, 'utf8');
|
|
17
16
|
const lines = content.split('\n');
|
|
18
17
|
for (const line of lines) {
|
|
19
|
-
const match =
|
|
18
|
+
const match = /^###\s*(T-\d+):\s*(.*)/.exec(line);
|
|
20
19
|
if (match) {
|
|
21
20
|
models[match[1]] = match[2].trim();
|
|
22
21
|
}
|
|
@@ -36,7 +35,7 @@ function formatUsageReport(wctx: WorkspaceContext): string {
|
|
|
36
35
|
|
|
37
36
|
try {
|
|
38
37
|
const usage: Record<string, number> = JSON.parse(fs.readFileSync(logPath, 'utf8'));
|
|
39
|
-
const totalTurns = usage
|
|
38
|
+
const totalTurns = usage._total_turns || 1;
|
|
40
39
|
const models = getModels(wctx);
|
|
41
40
|
|
|
42
41
|
let report = `# 🧠 Thinking OS — Usage Report\n\n`;
|
|
@@ -121,7 +120,7 @@ function formatAuditReport(wctx: WorkspaceContext): string {
|
|
|
121
120
|
|
|
122
121
|
try {
|
|
123
122
|
const usage: Record<string, number> = JSON.parse(fs.readFileSync(logPath, 'utf8'));
|
|
124
|
-
const totalTurns = usage
|
|
123
|
+
const totalTurns = usage._total_turns || 1;
|
|
125
124
|
|
|
126
125
|
const overused: string[] = [];
|
|
127
126
|
const underused: string[] = [];
|
|
@@ -20,6 +20,7 @@ function formatState(state: string): string {
|
|
|
20
20
|
return `${icon} ${state}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: debug output builder requires all context parameters - refactoring would break API
|
|
23
24
|
function buildOutput(
|
|
24
25
|
workflowId: string,
|
|
25
26
|
summary: ReturnType<InstanceType<typeof WorkflowStore>['getWorkflow']>,
|
|
@@ -87,7 +88,7 @@ export function handleWorkflowDebugCommand(
|
|
|
87
88
|
|
|
88
89
|
// Parse workflow ID from args
|
|
89
90
|
const args = (ctx as { args?: string }).args?.trim() || '';
|
|
90
|
-
const workflowId = args.split(/\s+/)
|
|
91
|
+
const [workflowId] = args.split(/\s+/);
|
|
91
92
|
|
|
92
93
|
if (!workflowId) {
|
|
93
94
|
return {
|
package/src/config/errors.ts
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# src/core/ — Domain Core
|
|
2
|
+
|
|
3
|
+
**27 TypeScript files.** Trust engine, evolution pipeline, pain calculation, config, trajectory DB, event log, hygiene tracking.
|
|
4
|
+
|
|
5
|
+
## WHERE TO LOOK
|
|
6
|
+
|
|
7
|
+
| Task | File | Notes |
|
|
8
|
+
|------|------|-------|
|
|
9
|
+
| Trust permissions | `trust-engine.ts` | 4-stage model, score floor=30, cold-start grace |
|
|
10
|
+
| Evolution points | `evolution-engine.ts` | 5-tier EP system (Seed→Forest), double-reward after recovery |
|
|
11
|
+
| Principle lifecycle | `evolution-reducer.ts` | Event sourcing → `evolution.jsonl` stream |
|
|
12
|
+
| Pain scoring | `pain.ts` | Tool failures + gate blocks → pain score |
|
|
13
|
+
| Config management | `config.ts`, `config-service.ts` | PainConfig, dot-notation `get()`, singleton factory |
|
|
14
|
+
| Trajectory analytics | `trajectory.ts` | SQLite (better-sqlite3), sessions/turns/tool_calls |
|
|
15
|
+
| Event logging | `event-log.ts` | JSONL buffered writes (20 entries or 30s flush) |
|
|
16
|
+
| Pain patterns | `dictionary.ts`, `dictionary-service.ts` | Regex + exact_match rules |
|
|
17
|
+
| Detection funnel | `detection-funnel.ts`, `detection-service.ts` | Text input queue |
|
|
18
|
+
| Session state | `session-tracker.ts` | GFI, token usage, stuck loop detection |
|
|
19
|
+
| Central facade | `workspace-context.ts` | `WorkspaceContext.fromHookContext(ctx)` — all services flow through here |
|
|
20
|
+
| Path resolution | `paths.ts`, `path-resolver.ts` | `api.resolvePath()` is the only compliant entry |
|
|
21
|
+
|
|
22
|
+
## CONVENTIONS
|
|
23
|
+
|
|
24
|
+
- **Singleton factory**: `XxxService.get(stateDir)` — cached per stateDir
|
|
25
|
+
- **WorkspaceContext facade**: lazy-initializes config, trust, eventLog, dictionary, hygiene, evolutionReducer, trajectory
|
|
26
|
+
- **Event sourcing**: EvolutionReducerImpl appends events → in-memory state update
|
|
27
|
+
- **File locking**: `withLock()` / `withLockAsync()` for critical state (evolution, trajectory, trust)
|
|
28
|
+
- **Buffered flush**: EventLog batches 20 entries or flushes every 30s
|
|
29
|
+
|
|
30
|
+
## ANTI-PATTERNS
|
|
31
|
+
|
|
32
|
+
- ❌ Never bypass WorkspaceContext — always go through the facade
|
|
33
|
+
- ❌ Never write to `.state/` files directly — use the service layer
|
|
34
|
+
- ❌ Never hardcode paths — use `paths.ts` / `path-resolver.ts`
|
|
@@ -300,6 +300,7 @@ export function getEffectiveThresholds(stateDir: string): ThresholdValues {
|
|
|
300
300
|
* @param reason - Reason for the adjustment (required for tracking)
|
|
301
301
|
* @returns UpdateThresholdResult
|
|
302
302
|
*/
|
|
303
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: threshold state update requires all parameters - refactoring would break API
|
|
303
304
|
export function updateThresholdState(
|
|
304
305
|
stateDir: string,
|
|
305
306
|
thresholdName: ThresholdName,
|
|
@@ -439,7 +440,7 @@ export function adjustThresholdsFromSignals(
|
|
|
439
440
|
currentThresholds.principleAlignmentMin + adjustment,
|
|
440
441
|
`High arbiter reject rate (${signals.arbiterRejectRate.toFixed(2)}) → tightening alignment threshold`
|
|
441
442
|
);
|
|
442
|
-
if (result.changed && (!bestResult.changed || (result.newValue! - result.oldValue!) > 0)) {
|
|
443
|
+
if (result.changed && (!bestResult.changed || ((result.newValue! - result.oldValue!) > 0))) { // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: changed flag guarantees newValue/oldValue are defined
|
|
443
444
|
bestResult = result;
|
|
444
445
|
}
|
|
445
446
|
}
|
|
@@ -453,7 +454,7 @@ export function adjustThresholdsFromSignals(
|
|
|
453
454
|
currentThresholds.executabilityMin + adjustment,
|
|
454
455
|
`High executability reject rate (${signals.executabilityRejectRate.toFixed(2)}) → tightening executability threshold`
|
|
455
456
|
);
|
|
456
|
-
if (result.changed && (!bestResult.changed || (result.newValue! - result.oldValue!) > 0)) {
|
|
457
|
+
if (result.changed && (!bestResult.changed || ((result.newValue! - result.oldValue!) > 0))) { // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: changed flag guarantees newValue/oldValue are defined
|
|
457
458
|
bestResult = result;
|
|
458
459
|
}
|
|
459
460
|
}
|
|
@@ -467,7 +468,7 @@ export function adjustThresholdsFromSignals(
|
|
|
467
468
|
Math.max(currentThresholds.aggregateMin - reward, THRESHOLD_MIN),
|
|
468
469
|
`Positive quality delta (${signals.qualityDelta.toFixed(2)}) → rewarding with slightly lower aggregate threshold`
|
|
469
470
|
);
|
|
470
|
-
if (result.changed && (!bestResult.changed || (result.oldValue! - result.newValue!) > 0)) {
|
|
471
|
+
if (result.changed && (!bestResult.changed || ((result.oldValue! - result.newValue!) > 0))) { // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: changed flag guarantees newValue/oldValue are defined
|
|
471
472
|
bestResult = result;
|
|
472
473
|
}
|
|
473
474
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Implementation Asset Storage
|
|
3
|
+
* ================================
|
|
4
|
+
*
|
|
5
|
+
* Manages versioned code implementation assets: manifests, entry files,
|
|
6
|
+
* and metadata pointers for Implementation(type=code) records.
|
|
7
|
+
*
|
|
8
|
+
* DESIGN CONSTRAINTS (per D-09 through D-12):
|
|
9
|
+
* - Manifest is loading metadata, NOT the source of truth for lifecycle state (D-11)
|
|
10
|
+
* - The Principle Tree ledger remains canonical for lifecycle and relationships (D-11)
|
|
11
|
+
* - Asset root follows the PD stateDir convention:
|
|
12
|
+
* {stateDir}/principles/implementations/{implId}/
|
|
13
|
+
* - All writes use withLock for atomicity (matching ledger pattern)
|
|
14
|
+
* - This module does NOT implement replay execution, evaluation report generation,
|
|
15
|
+
* or promotion logic (D-12)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { withLock } from '../utils/file-lock.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Manifest shape for a code implementation's filesystem assets.
|
|
28
|
+
* Subordinate to the ledger per D-11: does NOT store lifecycleState.
|
|
29
|
+
*/
|
|
30
|
+
export interface CodeImplementationManifest {
|
|
31
|
+
/** Asset version (distinct from Implementation.version in the ledger) */
|
|
32
|
+
version: string;
|
|
33
|
+
/** Relative filename of the entry point (e.g., 'entry.js') */
|
|
34
|
+
entryFile: string;
|
|
35
|
+
/** ISO timestamp of initial creation */
|
|
36
|
+
createdAt: string;
|
|
37
|
+
/** ISO timestamp of last manifest update */
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
/** Fingerprints of samples this implementation was tested against */
|
|
40
|
+
replaySampleRefs: string[];
|
|
41
|
+
/** Relative path to most recent eval report, or null */
|
|
42
|
+
lastEvalReportRef: string | null;
|
|
43
|
+
/** Provenance carried with the generated candidate assets */
|
|
44
|
+
lineage?: CodeImplementationLineageMetadata;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CodeImplementationLineageMetadata {
|
|
48
|
+
principleId: string;
|
|
49
|
+
ruleId: string;
|
|
50
|
+
sourceSnapshotRef: string;
|
|
51
|
+
sourcePainIds: string[];
|
|
52
|
+
sourceGateBlockIds: string[];
|
|
53
|
+
sourceSessionId: string;
|
|
54
|
+
artificerArtifactId: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Constants
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
const MANIFEST_FILENAME = 'manifest.json';
|
|
62
|
+
const ENTRY_FILENAME = 'entry.js';
|
|
63
|
+
const REPLAYS_DIRNAME = 'replays';
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Path validation
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate that an implId contains no path separators.
|
|
71
|
+
* implId comes from ledger record IDs (controlled namespace), but we
|
|
72
|
+
* validate to prevent path traversal (T-12-08).
|
|
73
|
+
*/
|
|
74
|
+
function validateImplId(implId: string): void {
|
|
75
|
+
if (implId.includes('/') || implId.includes('\\') || implId.includes('..')) {
|
|
76
|
+
throw new Error(`Invalid implementation ID: "${implId}" contains path separators`);
|
|
77
|
+
}
|
|
78
|
+
if (!implId) {
|
|
79
|
+
throw new Error('Implementation ID must not be empty');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
function ensureDir(dir: string): void {
|
|
88
|
+
if (!fs.existsSync(dir)) {
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Public API
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the absolute path for an implementation's asset root.
|
|
99
|
+
* Convention: {stateDir}/principles/implementations/{implId}/
|
|
100
|
+
*/
|
|
101
|
+
export function getImplementationAssetRoot(stateDir: string, implId: string): string {
|
|
102
|
+
validateImplId(implId);
|
|
103
|
+
return path.join(stateDir, 'principles', 'implementations', implId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load manifest from disk. Returns null if not found (does not throw).
|
|
108
|
+
*/
|
|
109
|
+
export function loadManifest(stateDir: string, implId: string): CodeImplementationManifest | null {
|
|
110
|
+
validateImplId(implId);
|
|
111
|
+
const manifestPath = path.join(getImplementationAssetRoot(stateDir, implId), MANIFEST_FILENAME);
|
|
112
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as CodeImplementationManifest;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Write manifest atomically using withLock.
|
|
122
|
+
*/
|
|
123
|
+
export function writeManifest(
|
|
124
|
+
stateDir: string,
|
|
125
|
+
implId: string,
|
|
126
|
+
manifest: CodeImplementationManifest,
|
|
127
|
+
): void {
|
|
128
|
+
validateImplId(implId);
|
|
129
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
130
|
+
const manifestPath = path.join(assetRoot, MANIFEST_FILENAME);
|
|
131
|
+
ensureDir(assetRoot);
|
|
132
|
+
withLock(manifestPath, () => {
|
|
133
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: entry source writer requires state + id + source - refactoring would break API
|
|
138
|
+
export function writeEntrySource(
|
|
139
|
+
stateDir: string,
|
|
140
|
+
implId: string,
|
|
141
|
+
sourceCode: string,
|
|
142
|
+
entryFile = ENTRY_FILENAME,
|
|
143
|
+
): void {
|
|
144
|
+
validateImplId(implId);
|
|
145
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
146
|
+
const entryPath = path.join(assetRoot, entryFile);
|
|
147
|
+
ensureDir(assetRoot);
|
|
148
|
+
withLock(entryPath, () => {
|
|
149
|
+
fs.writeFileSync(entryPath, sourceCode, 'utf-8');
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function deleteImplementationAssetDir(stateDir: string, implId: string): void {
|
|
154
|
+
validateImplId(implId);
|
|
155
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
156
|
+
if (!fs.existsSync(assetRoot)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
withLock(assetRoot, () => {
|
|
160
|
+
fs.rmSync(assetRoot, { recursive: true, force: true });
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Load the entry source code from disk.
|
|
166
|
+
* Returns null if manifest doesn't exist or entry file is missing.
|
|
167
|
+
*/
|
|
168
|
+
export function loadEntrySource(stateDir: string, implId: string): string | null {
|
|
169
|
+
validateImplId(implId);
|
|
170
|
+
const manifest = loadManifest(stateDir, implId);
|
|
171
|
+
if (!manifest) return null;
|
|
172
|
+
const entryPath = path.join(getImplementationAssetRoot(stateDir, implId), manifest.entryFile);
|
|
173
|
+
if (!fs.existsSync(entryPath)) return null;
|
|
174
|
+
try {
|
|
175
|
+
return fs.readFileSync(entryPath, 'utf-8');
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create the full asset directory structure for a new implementation.
|
|
183
|
+
*
|
|
184
|
+
* Creates:
|
|
185
|
+
* {implId}/ - asset root
|
|
186
|
+
* {implId}/entry.js - placeholder entry point (only if not already present)
|
|
187
|
+
* {implId}/manifest.json - asset manifest with version and timestamps
|
|
188
|
+
* {implId}/replays/ - empty directory for future replay reports
|
|
189
|
+
*
|
|
190
|
+
* Idempotent: calling again with the same implId will NOT overwrite an existing entry.js.
|
|
191
|
+
*/
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: asset dir creation requires state + id + version - refactoring would break API
|
|
193
|
+
export function createImplementationAssetDir(
|
|
194
|
+
stateDir: string,
|
|
195
|
+
implId: string,
|
|
196
|
+
version: string,
|
|
197
|
+
options: {
|
|
198
|
+
entrySource?: string;
|
|
199
|
+
lineage?: CodeImplementationLineageMetadata;
|
|
200
|
+
} = {},
|
|
201
|
+
): CodeImplementationManifest {
|
|
202
|
+
validateImplId(implId);
|
|
203
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
204
|
+
const replaysDir = path.join(assetRoot, REPLAYS_DIRNAME);
|
|
205
|
+
const entryPath = path.join(assetRoot, ENTRY_FILENAME);
|
|
206
|
+
const entrySource =
|
|
207
|
+
options.entrySource ??
|
|
208
|
+
[
|
|
209
|
+
'// Code implementation entry point',
|
|
210
|
+
'// Exports: meta (RuleHostMeta), evaluate (input: RuleHostInput) => RuleHostResult',
|
|
211
|
+
'// This file will be replaced by nocturnal candidate generation (Phase 14)',
|
|
212
|
+
'export const meta = { name: "placeholder", version: "0.0.1", ruleId: "", coversCondition: "" };',
|
|
213
|
+
'export function evaluate(input) { return { decision: "allow", matched: false, reason: "placeholder" }; }',
|
|
214
|
+
].join('\n');
|
|
215
|
+
|
|
216
|
+
withLock(assetRoot, () => {
|
|
217
|
+
ensureDir(assetRoot);
|
|
218
|
+
ensureDir(replaysDir);
|
|
219
|
+
if (!fs.existsSync(entryPath)) {
|
|
220
|
+
fs.writeFileSync(entryPath, entrySource, 'utf-8');
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (fs.existsSync(entryPath) && options.entrySource) {
|
|
225
|
+
writeEntrySource(stateDir, implId, options.entrySource);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const now = new Date().toISOString();
|
|
229
|
+
const manifest: CodeImplementationManifest = {
|
|
230
|
+
version,
|
|
231
|
+
entryFile: ENTRY_FILENAME,
|
|
232
|
+
createdAt: now,
|
|
233
|
+
updatedAt: now,
|
|
234
|
+
replaySampleRefs: [],
|
|
235
|
+
lastEvalReportRef: null,
|
|
236
|
+
...(options.lineage ? { lineage: options.lineage } : {}),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
writeManifest(stateDir, implId, manifest);
|
|
240
|
+
return manifest;
|
|
241
|
+
}
|
package/src/core/config.ts
CHANGED
|
@@ -230,7 +230,7 @@ export const DEFAULT_SETTINGS: PainSettings = {
|
|
|
230
230
|
|
|
231
231
|
export class PainConfig {
|
|
232
232
|
private settings: PainSettings = { ...DEFAULT_SETTINGS };
|
|
233
|
-
private filePath: string;
|
|
233
|
+
private readonly filePath: string;
|
|
234
234
|
|
|
235
235
|
constructor(stateDir: string) {
|
|
236
236
|
this.filePath = path.join(stateDir, 'pain_settings.json');
|
|
@@ -242,7 +242,7 @@ export class PainConfig {
|
|
|
242
242
|
const loaded = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
|
|
243
243
|
this.settings = this.deepMerge(DEFAULT_SETTINGS, loaded);
|
|
244
244
|
this.validate(this.settings);
|
|
245
|
-
} catch
|
|
245
|
+
} catch {
|
|
246
246
|
console.error('[PD] Failed to parse pain_settings.json, using defaults.');
|
|
247
247
|
}
|
|
248
248
|
} else {
|
|
@@ -264,6 +264,8 @@ export class PainConfig {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
268
|
+
// Reason: deepMerge handles arbitrary nested object structures where static typing cannot precisely capture recursive object shapes
|
|
267
269
|
private deepMerge(target: any, source: any): any {
|
|
268
270
|
const output = { ...target };
|
|
269
271
|
if (source && typeof source === 'object') {
|
|
@@ -288,6 +290,7 @@ export class PainConfig {
|
|
|
288
290
|
/**
|
|
289
291
|
* Basic validation for critical settings
|
|
290
292
|
*/
|
|
293
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: validate is a pure validation function that modifies settings in place, no this reference needed
|
|
291
294
|
private validate(settings: PainSettings): void {
|
|
292
295
|
// Ensure intervals are positive
|
|
293
296
|
if (settings.intervals.worker_poll_ms < 1000) settings.intervals.worker_poll_ms = 15 * 60 * 1000;
|