@rmrdeveloper/sideroom-pi 8.0.2 → 8.1.1

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,220 @@
1
+ import type { RuleId, RuleSeverity } from './catalog.ts';
2
+
3
+ export interface AddedLine {
4
+ readonly line: number;
5
+ readonly text: string;
6
+ }
7
+
8
+ export interface RuleViolation {
9
+ readonly ruleId: RuleId;
10
+ readonly severity: RuleSeverity;
11
+ readonly message: string;
12
+ readonly line: number;
13
+ }
14
+
15
+ export function addedLines(
16
+ previous: string | undefined,
17
+ next: string,
18
+ ): AddedLine[] {
19
+ const nextLines = next.split('\n');
20
+ if (previous === undefined) {
21
+ return nonBlankLines(nextLines);
22
+ }
23
+
24
+ const previousLines = previous.split('\n');
25
+ const insertedIndices = insertedLineIndices(previousLines, nextLines);
26
+ const availableAdditions = additionCounts(previousLines, nextLines);
27
+ const added: AddedLine[] = [];
28
+ const orderedIndices = [...insertedIndices].sort(
29
+ (left, right) => left - right,
30
+ );
31
+
32
+ for (const index of orderedIndices) {
33
+ const text = nextLines[index] ?? '';
34
+ const key = text.trim();
35
+ if (key.length === 0) {
36
+ continue;
37
+ }
38
+ const count = availableAdditions.get(key) ?? 0;
39
+ if (count === 0) {
40
+ continue;
41
+ }
42
+ availableAdditions.set(key, count - 1);
43
+ added.push({ line: index + 1, text });
44
+ }
45
+
46
+ return added;
47
+ }
48
+
49
+ function nonBlankLines(lines: readonly string[]): AddedLine[] {
50
+ const added: AddedLine[] = [];
51
+ for (let index = 0; index < lines.length; index += 1) {
52
+ const text = lines[index] ?? '';
53
+ const trimmedText = text.trim();
54
+ if (trimmedText.length === 0) {
55
+ continue;
56
+ }
57
+ added.push({ line: index + 1, text });
58
+ }
59
+ return added;
60
+ }
61
+
62
+ function insertedLineIndices(
63
+ previousLines: readonly string[],
64
+ nextLines: readonly string[],
65
+ ): number[] {
66
+ const previousKeys = previousLines.map((line) => line.trim());
67
+ const nextKeys = nextLines.map((line) => line.trim());
68
+ const trace: Map<number, number>[] = [];
69
+ const frontier = new Map<number, number>([[1, 0]]);
70
+ const maximumDistance = previousLines.length + nextLines.length;
71
+
72
+ for (let distance = 0; distance <= maximumDistance; distance += 1) {
73
+ for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
74
+ const down = frontier.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY;
75
+ const right =
76
+ (frontier.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY) + 1;
77
+ let previousIndex: number;
78
+ const shouldMoveDown =
79
+ diagonal === -distance || (diagonal !== distance && down > right);
80
+ if (shouldMoveDown) {
81
+ previousIndex = down;
82
+ } else {
83
+ previousIndex = right;
84
+ }
85
+ const hasFiniteIndex = Number.isFinite(previousIndex);
86
+ if (!hasFiniteIndex) {
87
+ previousIndex = 0;
88
+ }
89
+
90
+ let nextIndex = previousIndex - diagonal;
91
+ while (
92
+ previousIndex < previousKeys.length &&
93
+ nextIndex < nextKeys.length &&
94
+ previousKeys[previousIndex] === nextKeys[nextIndex]
95
+ ) {
96
+ previousIndex += 1;
97
+ nextIndex += 1;
98
+ }
99
+ frontier.set(diagonal, previousIndex);
100
+
101
+ if (
102
+ previousIndex >= previousKeys.length &&
103
+ nextIndex >= nextKeys.length
104
+ ) {
105
+ trace.push(new Map(frontier));
106
+ return backtrackInsertedIndices(
107
+ trace,
108
+ distance,
109
+ previousKeys,
110
+ nextKeys,
111
+ );
112
+ }
113
+ }
114
+ trace.push(new Map(frontier));
115
+ }
116
+
117
+ throw new Error('Failed to compute the line diff.');
118
+ }
119
+
120
+ function backtrackInsertedIndices(
121
+ trace: readonly Map<number, number>[],
122
+ distance: number,
123
+ previousKeys: readonly string[],
124
+ nextKeys: readonly string[],
125
+ ): number[] {
126
+ let previousIndex = previousKeys.length;
127
+ let nextIndex = nextKeys.length;
128
+ const insertedIndices: number[] = [];
129
+
130
+ for (
131
+ let currentDistance = distance;
132
+ currentDistance > 0;
133
+ currentDistance -= 1
134
+ ) {
135
+ const previousFrontier = trace[currentDistance - 1];
136
+ if (previousFrontier === undefined) {
137
+ throw new Error('Line diff trace is incomplete.');
138
+ }
139
+
140
+ const diagonal = previousIndex - nextIndex;
141
+ const down = previousFrontier.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY;
142
+ const right =
143
+ (previousFrontier.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY) + 1;
144
+ const shouldMoveDown =
145
+ diagonal === -currentDistance ||
146
+ (diagonal !== currentDistance && down > right);
147
+ const previousDiagonal = shouldMoveDown ? diagonal + 1 : diagonal - 1;
148
+ const previousOldIndex = previousFrontier.get(previousDiagonal);
149
+ if (previousOldIndex === undefined) {
150
+ throw new Error('Line diff trace has an invalid diagonal.');
151
+ }
152
+ const previousNextIndex = previousOldIndex - previousDiagonal;
153
+
154
+ while (
155
+ previousIndex > previousOldIndex &&
156
+ nextIndex > previousNextIndex &&
157
+ previousKeys[previousIndex - 1] === nextKeys[nextIndex - 1]
158
+ ) {
159
+ previousIndex -= 1;
160
+ nextIndex -= 1;
161
+ }
162
+
163
+ if (previousIndex === previousOldIndex) {
164
+ nextIndex -= 1;
165
+ insertedIndices.push(nextIndex);
166
+ } else {
167
+ previousIndex -= 1;
168
+ }
169
+ }
170
+
171
+ return insertedIndices;
172
+ }
173
+
174
+ function additionCounts(
175
+ previousLines: readonly string[],
176
+ nextLines: readonly string[],
177
+ ): Map<string, number> {
178
+ const previousCounts = countLineKeys(previousLines);
179
+ const nextCounts = countLineKeys(nextLines);
180
+ const additions = new Map<string, number>();
181
+
182
+ for (const [key, nextCount] of nextCounts) {
183
+ const previousCount = previousCounts.get(key) ?? 0;
184
+ additions.set(key, Math.max(nextCount - previousCount, 0));
185
+ }
186
+ return additions;
187
+ }
188
+
189
+ function countLineKeys(lines: readonly string[]): Map<string, number> {
190
+ const counts = new Map<string, number>();
191
+ for (const line of lines) {
192
+ const key = line.trim();
193
+ if (key.length === 0) {
194
+ continue;
195
+ }
196
+ counts.set(key, (counts.get(key) ?? 0) + 1);
197
+ }
198
+ return counts;
199
+ }
200
+
201
+ export function formatBlockReason(
202
+ path: string,
203
+ violations: readonly RuleViolation[],
204
+ ): string {
205
+ return [
206
+ `Blocked ${path}: sideroom rules violated.`,
207
+ ...violations.map((item) => `- ${item.message}`),
208
+ 'Fix the flagged lines and retry the mutation.',
209
+ ].join('\n');
210
+ }
211
+
212
+ export function formatWarnings(
213
+ path: string,
214
+ violations: readonly RuleViolation[],
215
+ ): string {
216
+ return [
217
+ `Sideroom rules flagged ${path}:`,
218
+ ...violations.map((item) => `- ${item.message}`),
219
+ ].join('\n');
220
+ }
@@ -5,7 +5,7 @@ import {
5
5
  TODO_ACTION,
6
6
  type TodoAction,
7
7
  type TodoItem,
8
- type TodoParams,
8
+ type TodoToolParameters,
9
9
  UI_UNAVAILABLE,
10
10
  } from './model.ts';
11
11
 
@@ -21,7 +21,7 @@ export interface TodoToolResult {
21
21
  }
22
22
 
23
23
  export function executeTodo(
24
- params: TodoParams,
24
+ params: TodoToolParameters,
25
25
  ctx: Pick<ExtensionContext, 'mode'>,
26
26
  currentItems: readonly TodoItem[],
27
27
  ): TodoToolResult {
@@ -2,7 +2,11 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
2
  import { Text } from '@earendil-works/pi-tui';
3
3
  import { executeTodo, type TodoToolDetails } from './execute.ts';
4
4
  import { registerGuardEvents } from './guards.ts';
5
- import { prepareTodoArguments, TOOL_NAME, TodoParamsSchema } from './model.ts';
5
+ import {
6
+ prepareTodoArguments,
7
+ TOOL_NAME,
8
+ TodoToolParametersSchema,
9
+ } from './model.ts';
6
10
  import {
7
11
  type BoardLifecycle,
8
12
  commitBoard,
@@ -59,7 +63,7 @@ function registerTodoTool(
59
63
  description: TODO_DESCRIPTION,
60
64
  promptSnippet: TODO_PROMPT_SNIPPET,
61
65
  promptGuidelines: TODO_PROMPT_GUIDELINES,
62
- parameters: TodoParamsSchema,
66
+ parameters: TodoToolParametersSchema,
63
67
  prepareArguments: prepareTodoArguments,
64
68
  executionMode: 'sequential',
65
69
  execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
@@ -27,6 +27,11 @@ const TodoStatusSchema = Type.Union([
27
27
  Type.Literal(TODO_STATUS.cancelled),
28
28
  ]);
29
29
 
30
+ const TodoActionSchema = Type.Union([
31
+ Type.Literal(TODO_ACTION.propose),
32
+ Type.Literal(TODO_ACTION.update),
33
+ ]);
34
+
30
35
  const TodoItemSchema = Type.Object({
31
36
  id: Type.String({ description: 'Stable identifier for this item' }),
32
37
  content: Type.String({ description: 'Short description of the work' }),
@@ -41,28 +46,53 @@ const TodoPatchSchema = Type.Object({
41
46
  ),
42
47
  });
43
48
 
49
+ const TodoItemsField = Type.Array(TodoItemSchema, {
50
+ maxItems: MAX_TODO_ITEMS,
51
+ description: 'Replacement board for propose, with at most 20 items',
52
+ });
53
+
54
+ const TodoPatchesField = Type.Array(TodoPatchSchema, {
55
+ minItems: 1,
56
+ description: 'Patches to apply for update',
57
+ });
58
+
44
59
  const ProposeParamsSchema = Type.Object({
45
60
  action: Type.Literal(TODO_ACTION.propose),
46
- items: Type.Array(TodoItemSchema, {
47
- maxItems: MAX_TODO_ITEMS,
48
- description: 'Replacement board, with at most 20 items',
49
- }),
61
+ items: TodoItemsField,
50
62
  });
51
63
 
52
64
  const UpdateParamsSchema = Type.Object({
53
65
  action: Type.Literal(TODO_ACTION.update),
54
- patches: Type.Array(TodoPatchSchema, {
55
- minItems: 1,
56
- description: 'Patches to apply together',
57
- }),
66
+ patches: TodoPatchesField,
58
67
  });
59
68
 
69
+ /**
70
+ * Strict validator and types for sideroom_todo arguments. Kept as a
71
+ * discriminated union so invalid action/field combinations are rejected
72
+ * exactly. This schema is not registered with Pi; see
73
+ * TodoToolParametersSchema.
74
+ */
60
75
  export const TodoParamsSchema = Type.Union([
61
76
  ProposeParamsSchema,
62
77
  UpdateParamsSchema,
63
78
  ]);
64
79
 
80
+ /**
81
+ * Tool-facing schema registered with Pi. It must stay a top-level object:
82
+ * Claude Code silently drops every tool of an MCP server when one tool's
83
+ * inputSchema has a non-object top level (anyOf/oneOf/allOf), which is exactly
84
+ * what TodoParamsSchema emits. The discriminated union is still enforced at
85
+ * runtime by parseTodoParams. See docs/extensions/todo.md ("Tool schema
86
+ * compatibility") for the bug references and the condition to revert.
87
+ */
88
+ export const TodoToolParametersSchema = Type.Object({
89
+ action: TodoActionSchema,
90
+ items: Type.Optional(TodoItemsField),
91
+ patches: Type.Optional(TodoPatchesField),
92
+ });
93
+
65
94
  export type TodoParams = Static<typeof TodoParamsSchema>;
95
+ export type TodoToolParameters = Static<typeof TodoToolParametersSchema>;
66
96
  export type TodoAction = (typeof TODO_ACTION)[keyof typeof TODO_ACTION];
67
97
  export type TodoStatus = (typeof TODO_STATUS)[keyof typeof TODO_STATUS];
68
98
 
@@ -127,8 +157,8 @@ type JsonWireValue =
127
157
  | readonly JsonWireValue[]
128
158
  | JsonWireObject;
129
159
 
130
- export function prepareTodoArguments(args: unknown): TodoParams {
131
- return decodeTodoWireArgs(args) as TodoParams;
160
+ export function prepareTodoArguments(args: unknown): TodoToolParameters {
161
+ return decodeTodoWireArgs(args) as TodoToolParameters;
132
162
  }
133
163
 
134
164
  function decodeTodoWireArgs(args: unknown): JsonWireValue {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rmrdeveloper/sideroom-pi",
3
- "version": "8.0.2",
4
- "description": "A global Pi package with a questionnaire, work board, edited-file view, and coding guidelines for parent agents.",
3
+ "version": "8.1.1",
4
+ "description": "A global Pi package with a questionnaire, work board, edited-file view, coding guidelines, rule enforcement, and a finish gate for parent agents.",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "extensions",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sideroom-guidelines
3
- description: "Apply Sideroom's mandatory coding workflow before implementing, refactoring, or fixing Java, Laravel/PHP, TypeScript/TSX, Python, Go, or Rust code. Load this skill and the exact language guide before mutation. Do not use for unrelated prose or domain-only discussions."
3
+ description: "Apply Sideroom's mandatory coding workflow before implementing, refactoring, or fixing Java, Laravel/PHP, JavaScript/JSX, TypeScript/TSX, Python, Go, or Rust code. Load this skill and the exact language guide before mutation. Do not use for unrelated prose or domain-only discussions."
4
4
  license: MIT
5
5
  metadata:
6
6
  author: RMRdeveloper
@@ -26,6 +26,7 @@ Load this whole skill with `read`, without `offset` or `limit`, before the first
26
26
  | Path | Read |
27
27
  | ------ | ------ |
28
28
  | `.java` | `references/languages/java.md` |
29
+ | `.js` `.jsx` `.mjs` `.cjs` | `references/languages/javascript.md` |
29
30
  | `.php` | `references/languages/php-laravel.md` |
30
31
  | `.ts` `.tsx` | `references/languages/typescript.md` |
31
32
  | `.py` | `references/languages/python.md` |
@@ -74,6 +75,7 @@ Changed code follows every applicable rule in the loaded complete guide. The fin
74
75
  ## References
75
76
 
76
77
  - `references/languages/java.md`
78
+ - `references/languages/javascript.md`
77
79
  - `references/languages/php-laravel.md`
78
80
  - `references/languages/typescript.md`
79
81
  - `references/languages/python.md`