principles-disciple 1.157.0 → 1.159.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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * PRI-482 Phase 3 — Production RuleContext v2 assembler
3
+ *
4
+ * Converts raw TrajectoryDatabase rows into a validated RuleHistoryWindow,
5
+ * and provides buildProductionRuleContext for gate integration.
6
+ *
7
+ * ERR prevention:
8
+ * - ERR-001: every row field is validated as unknown. No `as` bypass on
9
+ * parsed params_json or outcome.
10
+ * - ERR-024: buildProductionRuleContext fail-soft — query failure returns
11
+ * unavailable, never throws.
12
+ * - ERR-025: callers use recordToolCall → getRuleHostContextRows → this
13
+ * assembler, covering the full production chain.
14
+ *
15
+ * Spec: docs/superpowers/specs/2026-06-27-rulecode-context-vision-design.md §5.
16
+ */
17
+ import type { RuleContextV2, RuleHistoryWindow } from '@principles/core/runtime-v2';
18
+ import type { RuleHostContextResult, RuleHostContextRow } from './trajectory-types.js';
19
+ /**
20
+ * Minimal interface for the data source that TrajectoryDatabase satisfies.
21
+ * Allows testing buildProductionRuleContext without a real DB.
22
+ */
23
+ export interface RuleContextDataSource {
24
+ getRuleHostContextRows(sessionId: string, limit: number): RuleHostContextResult;
25
+ }
26
+ /**
27
+ * Convert raw DB rows into a validated RuleHistoryWindow.
28
+ *
29
+ * If ANY row is malformed (bad params_json, invalid outcome, empty tool_name),
30
+ * the entire window is marked unavailable (fail loud, spec §5.2).
31
+ *
32
+ * ERR-001: row fields are validated as unknown. params_json is JSON.parsed
33
+ * then structurally checked (must be a non-array object). No `as` bypass.
34
+ */
35
+ export declare function assembleHistoryFromRows(rows: readonly RuleHostContextRow[], truncated: boolean, projectDir: string): RuleHistoryWindow;
36
+ /**
37
+ * Build a RuleContextV2 from the production data source (TrajectoryDatabase).
38
+ *
39
+ * ERR-024: if the query or assembly fails, returns an unavailable context
40
+ * (fail-soft). Never throws.
41
+ *
42
+ * sameActionBlockCount is always null in this version (spec §5.4:
43
+ * session-tracker.blockedAttempts is a session total, not per-action).
44
+ */
45
+ export declare function buildProductionRuleContext(sessionId: string | null | undefined, targetPath: string | null, source: RuleContextDataSource, projectDir: string, limit?: number): RuleContextV2;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * PRI-482 Phase 3 — Production RuleContext v2 assembler
3
+ *
4
+ * Converts raw TrajectoryDatabase rows into a validated RuleHistoryWindow,
5
+ * and provides buildProductionRuleContext for gate integration.
6
+ *
7
+ * ERR prevention:
8
+ * - ERR-001: every row field is validated as unknown. No `as` bypass on
9
+ * parsed params_json or outcome.
10
+ * - ERR-024: buildProductionRuleContext fail-soft — query failure returns
11
+ * unavailable, never throws.
12
+ * - ERR-025: callers use recordToolCall → getRuleHostContextRows → this
13
+ * assembler, covering the full production chain.
14
+ *
15
+ * Spec: docs/superpowers/specs/2026-06-27-rulecode-context-vision-design.md §5.
16
+ */
17
+ import { canonicalizeToolKind, computeBehaviorFacts, extractFilePathFromParams, normalizePathPure, UNAVAILABLE_RULE_CONTEXT, } from '@principles/core/runtime-v2';
18
+ // ── internal constants ─────────────────────────────────────────────────────
19
+ const VALID_OUTCOMES = new Set([
20
+ 'success',
21
+ 'failure',
22
+ 'blocked',
23
+ ]);
24
+ const DEFAULT_HISTORY_LIMIT = 20;
25
+ // ── type guards (rc-2: no `as` bypass) ─────────────────────────────────────
26
+ function isPlainObject(value) {
27
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
28
+ }
29
+ function isRuleToolOutcome(value) {
30
+ return typeof value === 'string' && VALID_OUTCOMES.has(value);
31
+ }
32
+ // ── assembleHistoryFromRows ────────────────────────────────────────────────
33
+ /**
34
+ * Convert raw DB rows into a validated RuleHistoryWindow.
35
+ *
36
+ * If ANY row is malformed (bad params_json, invalid outcome, empty tool_name),
37
+ * the entire window is marked unavailable (fail loud, spec §5.2).
38
+ *
39
+ * ERR-001: row fields are validated as unknown. params_json is JSON.parsed
40
+ * then structurally checked (must be a non-array object). No `as` bypass.
41
+ */
42
+ export function assembleHistoryFromRows(rows, truncated, projectDir) {
43
+ const records = [];
44
+ for (const row of rows) {
45
+ // ── validate tool_name (ERR-001: unknown) ──
46
+ const toolName = row.toolName;
47
+ if (typeof toolName !== 'string' || toolName.length === 0) {
48
+ return unavailable(`row id=${row.id}: tool_name must be a non-empty string`);
49
+ }
50
+ // ── validate outcome (ERR-001: unknown, enum check) ──
51
+ const outcome = row.outcome;
52
+ if (!isRuleToolOutcome(outcome)) {
53
+ return unavailable(`row id=${row.id}: outcome "${row.outcome}" is not a valid RuleToolOutcome`);
54
+ }
55
+ // ── validate & parse params_json (ERR-001: unknown) ──
56
+ const paramsJsonStr = row.paramsJson;
57
+ if (typeof paramsJsonStr !== 'string') {
58
+ return unavailable(`row id=${row.id}: params_json must be a string`);
59
+ }
60
+ let parsedParams;
61
+ try {
62
+ parsedParams = JSON.parse(paramsJsonStr);
63
+ }
64
+ catch {
65
+ return unavailable(`row id=${row.id}: params_json is not valid JSON`);
66
+ }
67
+ // Must be a non-array object (spec §5.2)
68
+ if (!isPlainObject(parsedParams)) {
69
+ return unavailable(`row id=${row.id}: params_json must parse to a non-array object`);
70
+ }
71
+ // ── build RuleToolCallRecord ──
72
+ const canonicalKind = canonicalizeToolKind(toolName);
73
+ // Extract normalized path using the same pure logic as buildRuleHostAction
74
+ // (avoids production/replay drift, spec §4.4)
75
+ const rawPath = extractFilePathFromParams(parsedParams, {
76
+ isBashTool: canonicalKind === 'execute',
77
+ isWriteTool: canonicalKind === 'write',
78
+ toolName,
79
+ });
80
+ const normalizedPath = normalizePathPure(rawPath, projectDir);
81
+ records.push({
82
+ sequenceId: row.id,
83
+ toolName,
84
+ canonicalKind,
85
+ normalizedPath: normalizedPath === '' ? null : normalizedPath,
86
+ paramsSummary: parsedParams,
87
+ outcome,
88
+ });
89
+ }
90
+ return {
91
+ status: 'available',
92
+ truncated,
93
+ calls: records,
94
+ };
95
+ }
96
+ // ── buildProductionRuleContext ─────────────────────────────────────────────
97
+ /**
98
+ * Build a RuleContextV2 from the production data source (TrajectoryDatabase).
99
+ *
100
+ * ERR-024: if the query or assembly fails, returns an unavailable context
101
+ * (fail-soft). Never throws.
102
+ *
103
+ * sameActionBlockCount is always null in this version (spec §5.4:
104
+ * session-tracker.blockedAttempts is a session total, not per-action).
105
+ */
106
+ export function buildProductionRuleContext(sessionId, targetPath, source, projectDir, limit = DEFAULT_HISTORY_LIMIT) {
107
+ // Guard: no session → no context
108
+ if (!sessionId || typeof sessionId !== 'string' || sessionId.length === 0) {
109
+ return UNAVAILABLE_RULE_CONTEXT;
110
+ }
111
+ try {
112
+ const result = source.getRuleHostContextRows(sessionId, limit);
113
+ const history = assembleHistoryFromRows(result.rows, result.truncated, projectDir);
114
+ // sameActionBlockCount = null (spec §5.4 — no reliable per-action source)
115
+ const facts = computeBehaviorFacts(history, targetPath, null);
116
+ return {
117
+ version: 2,
118
+ history,
119
+ facts,
120
+ };
121
+ }
122
+ catch {
123
+ // ERR-024: fail-soft — never let a DB error propagate to the rule host
124
+ return unavailableContext('query or assembly failed');
125
+ }
126
+ }
127
+ // ── internal helpers ───────────────────────────────────────────────────────
128
+ function unavailable(reason) {
129
+ return {
130
+ status: 'unavailable',
131
+ unavailableReason: reason,
132
+ truncated: false,
133
+ calls: [],
134
+ };
135
+ }
136
+ function unavailableContext(reason) {
137
+ return {
138
+ version: 2,
139
+ history: unavailable(reason),
140
+ facts: {
141
+ priorReadOfTarget: 'unknown',
142
+ readCount: null,
143
+ writeCount: null,
144
+ uniqueWritePathCount: null,
145
+ sameActionBlockCount: null,
146
+ },
147
+ };
148
+ }
@@ -222,4 +222,22 @@ export interface TrajectoryDatabaseOptions {
222
222
  busyTimeoutMs?: number;
223
223
  orphanBlobGraceDays?: number;
224
224
  }
225
+ /**
226
+ * Raw row from tool_calls for RuleContext v2 history assembly.
227
+ * Fields are raw strings from SQLite — the assembler validates them as unknown.
228
+ */
229
+ export interface RuleHostContextRow {
230
+ readonly id: number;
231
+ readonly toolName: string;
232
+ readonly outcome: string;
233
+ readonly paramsJson: string;
234
+ }
235
+ /**
236
+ * Result of getRuleHostContextRows: FIFO-ordered rows + truncated flag.
237
+ * truncated=true when more than `limit` rows existed (limit+1 read trick).
238
+ */
239
+ export interface RuleHostContextResult {
240
+ readonly rows: readonly RuleHostContextRow[];
241
+ readonly truncated: boolean;
242
+ }
225
243
  export type { DailyMetricRow };
@@ -1,5 +1,5 @@
1
- import type { CorrectionSampleReviewStatus, CorrectionExportMode, TrajectoryDataStats, TrajectoryAssistantTurnInput, TrajectoryUserTurnInput, TrajectoryToolCallInput, TrajectoryPainEventInput, TrajectoryGateBlockInput, TrajectoryTrustChangeInput, TrajectoryPrincipleEventInput, TrajectoryTaskOutcomeInput, TrajectorySessionInput, EvolutionTaskInput, EvolutionEventInput, EvolutionTaskRecord, EvolutionEventRecord, EvolutionTaskFilters, AssistantTurnRecord, CorrectionSampleRecord, TrajectoryExportResult, TrajectoryDatabaseOptions } from './trajectory-types.js';
2
- export type { CorrectionSampleReviewStatus, CorrectionExportMode, TrajectoryDataStats, TrajectoryAssistantTurnInput, TrajectoryUserTurnInput, TrajectoryToolCallInput, TrajectoryPainEventInput, TrajectoryGateBlockInput, DailyMetricRow, TrajectoryTrustChangeInput, TrajectoryPrincipleEventInput, TrajectoryTaskOutcomeInput, TrajectorySessionInput, TaskKind, TaskPriority, EvolutionTaskInput, EvolutionEventInput, EvolutionTaskRecord, EvolutionEventRecord, EvolutionTaskFilters, AssistantTurnRecord, CorrectionSampleRecord, TrajectoryExportResult, TrajectoryDatabaseOptions, } from './trajectory-types.js';
1
+ import type { CorrectionSampleReviewStatus, CorrectionExportMode, TrajectoryDataStats, TrajectoryAssistantTurnInput, TrajectoryUserTurnInput, TrajectoryToolCallInput, TrajectoryPainEventInput, TrajectoryGateBlockInput, TrajectoryTrustChangeInput, TrajectoryPrincipleEventInput, TrajectoryTaskOutcomeInput, TrajectorySessionInput, EvolutionTaskInput, EvolutionEventInput, EvolutionTaskRecord, EvolutionEventRecord, EvolutionTaskFilters, AssistantTurnRecord, CorrectionSampleRecord, TrajectoryExportResult, TrajectoryDatabaseOptions, RuleHostContextResult } from './trajectory-types.js';
2
+ export type { CorrectionSampleReviewStatus, CorrectionExportMode, TrajectoryDataStats, TrajectoryAssistantTurnInput, TrajectoryUserTurnInput, TrajectoryToolCallInput, TrajectoryPainEventInput, TrajectoryGateBlockInput, DailyMetricRow, TrajectoryTrustChangeInput, TrajectoryPrincipleEventInput, TrajectoryTaskOutcomeInput, TrajectorySessionInput, TaskKind, TaskPriority, EvolutionTaskInput, EvolutionEventInput, EvolutionTaskRecord, EvolutionEventRecord, EvolutionTaskFilters, AssistantTurnRecord, CorrectionSampleRecord, TrajectoryExportResult, TrajectoryDatabaseOptions, RuleHostContextRow, RuleHostContextResult, } from './trajectory-types.js';
3
3
  /**
4
4
  * Initialize trajectory.db schema at the given workspace directory.
5
5
  *
@@ -121,6 +121,15 @@ export declare class TrajectoryDatabase {
121
121
  resultPreview: string | null;
122
122
  createdAt: string;
123
123
  }[];
124
+ /**
125
+ * PRI-482 Phase 3: Query tool_calls for RuleContext v2 history assembly.
126
+ *
127
+ * Reads limit+1 rows (DESC by id) to compute truncated, then reverses to FIFO.
128
+ * Returns raw rows — the assembler (rule-context-assembler.ts) validates them.
129
+ *
130
+ * Spec: §5.1, §5.2. ERR-026: reuses production schema (no hand-written DDL).
131
+ */
132
+ getRuleHostContextRows(sessionId: string, limit?: number): RuleHostContextResult;
124
133
  /**
125
134
  * List pain events for a session.
126
135
  *
@@ -903,6 +903,36 @@ export class TrajectoryDatabase {
903
903
  };
904
904
  });
905
905
  }
906
+ /**
907
+ * PRI-482 Phase 3: Query tool_calls for RuleContext v2 history assembly.
908
+ *
909
+ * Reads limit+1 rows (DESC by id) to compute truncated, then reverses to FIFO.
910
+ * Returns raw rows — the assembler (rule-context-assembler.ts) validates them.
911
+ *
912
+ * Spec: §5.1, §5.2. ERR-026: reuses production schema (no hand-written DDL).
913
+ */
914
+ getRuleHostContextRows(sessionId, limit = 20) {
915
+ const cappedLimit = Math.max(1, Math.floor(limit));
916
+ const rows = this.db.prepare(`
917
+ SELECT id, tool_name, outcome, params_json
918
+ FROM tool_calls
919
+ WHERE session_id = ?
920
+ ORDER BY id DESC
921
+ LIMIT ?
922
+ `).all(sessionId, cappedLimit + 1);
923
+ const truncated = rows.length > cappedLimit;
924
+ const output = truncated ? rows.slice(0, cappedLimit) : rows;
925
+ output.reverse(); // DESC → FIFO (oldest first)
926
+ return {
927
+ rows: output.map((row) => ({
928
+ id: Number(row.id),
929
+ toolName: String(row.tool_name),
930
+ outcome: String(row.outcome),
931
+ paramsJson: String(row.params_json),
932
+ })),
933
+ truncated,
934
+ };
935
+ }
906
936
  /**
907
937
  * List pain events for a session.
908
938
  *
@@ -17,7 +17,7 @@ import { AGENT_TOOLS, BASH_TOOLS_SET, WRITE_TOOLS } from '../constants/tools.js'
17
17
  import { getSession, hasRecentThinking } from '../core/session-tracker.js';
18
18
  import { getEvolutionEngine } from '../core/evolution-engine.js';
19
19
  import { EventLogService } from '../core/event-log.js';
20
- import { estimateLineChanges } from '../core/risk-calculator.js';
20
+ import { estimateLineChanges } from '@principles/core/runtime-v2';
21
21
  export function handleBeforeToolCall(event, ctx) {
22
22
  const logger = ctx.logger || console;
23
23
  // 1. Identify tool type
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.157.0",
5
+ "version": "1.159.0",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.157.0",
3
+ "version": "1.159.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,22 +0,0 @@
1
- export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
2
- export interface FileModification {
3
- toolName: string;
4
- params: Record<string, unknown>;
5
- }
6
- export declare function estimateLineChanges(modification: FileModification): number;
7
- export declare function assessRiskLevel(filePath: string, modification: FileModification, riskPaths: string[]): RiskLevel;
8
- /**
9
- * Get the total line count of a target file.
10
- * @param absoluteFilePath - Absolute path to the file
11
- * @returns File line count, or null if file doesn't exist or can't be read
12
- */
13
- export declare function getTargetFileLineCount(absoluteFilePath: string): number | null;
14
- /**
15
- * Calculate the effective line limit based on percentage of target file.
16
- * @param targetLineCount - Total lines in target file
17
- * @param percentage - Allowed percentage (0-100)
18
- * @param minLines - Absolute minimum threshold
19
- * @param maxLines - Optional upper bound to prevent misconfiguration
20
- * @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
21
- */
22
- export declare function calculatePercentageThreshold(targetLineCount: number, percentage: number, minLines: number, maxLines?: number): number;
@@ -1,88 +0,0 @@
1
- /* global NodeJS */
2
- import * as fs from 'fs';
3
- import { isRisky } from '../utils/io.js';
4
- export function estimateLineChanges(modification) {
5
- const { toolName, params } = modification;
6
- if (toolName === 'write_file' || toolName === 'write') {
7
- const content = params.content || '';
8
- return content.split('\n').length;
9
- }
10
- if (toolName === 'replace' || toolName === 'edit') {
11
- const newContent = params.new_string || params.newText || '';
12
- return newContent.split('\n').length;
13
- }
14
- if (toolName === 'apply_patch' || toolName === 'patch') {
15
- const patch = params.patch || '';
16
- // Rough estimate for patch files
17
- return patch.split('\n').filter((l) => l.startsWith('+') || l.startsWith('-')).length;
18
- }
19
- if (toolName === 'delete_file') {
20
- // Deleting a file is considered a significant change, but we don't know the size.
21
- // We'll treat it as a medium-to-large size change.
22
- return 50;
23
- }
24
- return 0;
25
- }
26
- export function assessRiskLevel(filePath, modification, riskPaths) {
27
- const isRiskPath = isRisky(filePath, riskPaths);
28
- const estimatedLines = estimateLineChanges(modification);
29
- if (isRiskPath) {
30
- if (estimatedLines > 100)
31
- return 'CRITICAL';
32
- return 'HIGH';
33
- }
34
- else {
35
- if (estimatedLines > 100)
36
- return 'HIGH';
37
- if (estimatedLines > 10)
38
- return 'MEDIUM';
39
- return 'LOW';
40
- }
41
- }
42
- /**
43
- * Get the total line count of a target file.
44
- * @param absoluteFilePath - Absolute path to the file
45
- * @returns File line count, or null if file doesn't exist or can't be read
46
- */
47
- export function getTargetFileLineCount(absoluteFilePath) {
48
- try {
49
- if (!fs.existsSync(absoluteFilePath)) {
50
- return null; // File genuinely doesn't exist
51
- }
52
- const stats = fs.statSync(absoluteFilePath);
53
- if (!stats.isFile()) {
54
- return null; // Not a regular file (directory, device, etc.)
55
- }
56
- const content = fs.readFileSync(absoluteFilePath, 'utf-8');
57
- return content.split('\n').length;
58
- }
59
- catch (e) {
60
- // Log error before falling back to null - this is intentional for security gates
61
- const error = e instanceof Error ? e : new Error(String(e));
62
- const errorCode = e.code;
63
- console.error(`[PD:RISK_CALC] Failed to read file for line count: ${absoluteFilePath}`, {
64
- code: errorCode,
65
- message: error.message,
66
- });
67
- return null;
68
- }
69
- }
70
- /**
71
- * Calculate the effective line limit based on percentage of target file.
72
- * @param targetLineCount - Total lines in target file
73
- * @param percentage - Allowed percentage (0-100)
74
- * @param minLines - Absolute minimum threshold
75
- * @param maxLines - Optional upper bound to prevent misconfiguration
76
- * @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
77
- */
78
- export function calculatePercentageThreshold(targetLineCount, percentage, minLines, maxLines) {
79
- // Clamp percentage to valid range [0, 100]
80
- const clampedPercentage = Math.max(0, Math.min(100, percentage));
81
- const calculated = Math.round(targetLineCount * (clampedPercentage / 100));
82
- let effectiveLimit = Math.max(calculated, minLines);
83
- // Apply optional upper bound
84
- if (maxLines !== undefined && maxLines > 0) {
85
- effectiveLimit = Math.min(effectiveLimit, maxLines);
86
- }
87
- return effectiveLimit;
88
- }