@rmrdeveloper/sideroom-pi 8.6.0 → 8.8.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/README.md +34 -1
- package/extensions/ask/index.ts +2 -2
- package/extensions/monorepo-skills/flags.ts +9 -0
- package/extensions/monorepo-skills/index.ts +36 -0
- package/extensions/monorepo-skills/prompt.ts +14 -0
- package/extensions/monorepo-skills/scan.ts +225 -0
- package/package.json +4 -1
- package/skills/sideroom-architecture/SKILL.md +104 -0
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Sideroom answers each one with a small, opinionated surface:
|
|
|
52
52
|
| Invisible work | `sideroom_todo` — a live board above the editor, always one step in focus |
|
|
53
53
|
| Repo pollution | Session-branch state — the board and history die with the session, never with a commit |
|
|
54
54
|
| Style drift | A pre-edit gate + the on-demand `sideroom-guidelines` skill |
|
|
55
|
+
| Invisible monorepo skills | `monorepo-skills` — trusted child folders join Pi's available skill list |
|
|
55
56
|
| Vague plans | `sideroom-grill` — an interview that settles the words before the work |
|
|
56
57
|
| Unapplied guidelines | `sideroom_rules` — mechanical checks that block or flag the lines you add |
|
|
57
58
|
| Unclear answers | `sideroom_persona` — one voice: direct, plain, and free of jargon it invented |
|
|
@@ -86,6 +87,10 @@ wall of tool calls, it:
|
|
|
86
87
|
blocked. The guides cover guard clauses, braced conditionals, fail-fast
|
|
87
88
|
errors, focused units, and all 19 canonical rules without dumping them into
|
|
88
89
|
the system prompt.
|
|
90
|
+
- **Finds the skills below the working directory.** In a trusted monorepo, Pi
|
|
91
|
+
normally ignores project skills stored under child folders. Sideroom scans
|
|
92
|
+
three levels down, adds their `.pi/skills` and `.agents/skills`, and leaves
|
|
93
|
+
duplicate-name warnings to Pi.
|
|
89
94
|
- **Cannot sneak sloppy lines past the gate.** Each write and edit is checked
|
|
90
95
|
against the mechanical rules on the added lines only: braceless
|
|
91
96
|
conditionals and swallowed errors block the mutation; vague names, stale
|
|
@@ -154,6 +159,19 @@ Each guide mirrors all 19 rules in the canonical seed with idiomatic examples.
|
|
|
154
159
|
The seed, `assets/artifacts/GUIDELINES_TEMPLATE.md`, is never pasted into the
|
|
155
160
|
system prompt.
|
|
156
161
|
|
|
162
|
+
### Monorepo skills — child skills are still project skills
|
|
163
|
+
|
|
164
|
+
Pi discovers `.pi/skills` only from its working directory and
|
|
165
|
+
`.agents/skills` from that directory and its ancestors. `monorepo-skills`
|
|
166
|
+
closes the downward gap: in a trusted project it scans at most three child
|
|
167
|
+
levels, respects `.gitignore`, `.ignore`, and `.fdignore`, and contributes the
|
|
168
|
+
same skill locations without writing project configuration. Skill bodies stay
|
|
169
|
+
on demand; only names, descriptions, and locations enter the system prompt.
|
|
170
|
+
|
|
171
|
+
Duplicate names keep Pi's precedence and diagnostics. Root and global skills
|
|
172
|
+
load before extension paths, so use `pi config -l` to disable one that shadows
|
|
173
|
+
a needed child-folder skill.
|
|
174
|
+
|
|
157
175
|
### Rules — enforcement, not advice
|
|
158
176
|
|
|
159
177
|
The guidelines gate makes the agent read the rules; `sideroom_rules` makes it
|
|
@@ -272,6 +290,19 @@ languages use the shared table. Do not route around
|
|
|
272
290
|
the gate through Bash or another file-mutation path. Before finishing, review
|
|
273
291
|
the diff against the loaded guide and run the relevant project checks.
|
|
274
292
|
|
|
293
|
+
### Monorepo-skills contract
|
|
294
|
+
|
|
295
|
+
- Trigger: `resources_discover` after project trust resolves, in every mode.
|
|
296
|
+
- Scope: `.pi/skills` and `.agents/skills` in child folders at most three
|
|
297
|
+
levels below Pi's working directory.
|
|
298
|
+
- Traversal: ignore-aware, no directory symlinks, deterministic child-folder
|
|
299
|
+
order, `.pi/skills` before `.agents/skills` inside one folder.
|
|
300
|
+
- Safety: untrusted projects and CLI `--no-skills`/`-ns` contribute no paths.
|
|
301
|
+
- Collisions: Pi keeps the first name and reports skipped paths. Child-folder
|
|
302
|
+
skills cannot override root or user skills added earlier.
|
|
303
|
+
- Prompt: one static, idempotent note prefers the skill whose location matches
|
|
304
|
+
the folder being edited.
|
|
305
|
+
|
|
275
306
|
### Rules contract
|
|
276
307
|
|
|
277
308
|
- Trigger: every `write`/`edit` call, in every mode.
|
|
@@ -338,7 +369,9 @@ skills. If a third-party skill still wins, remove it from routing by adding
|
|
|
338
369
|
`disable-model-invocation: true` to its frontmatter, or exclude its path with
|
|
339
370
|
`-path` in `settings.skills`. A literal name collision keeps the first skill
|
|
340
371
|
found: project `.pi/skills`, then project `.agents/skills`, then
|
|
341
|
-
`~/.pi/agent/skills`, then `~/.agents/skills`, then packages.
|
|
372
|
+
`~/.pi/agent/skills`, then `~/.agents/skills`, then packages. Child-folder
|
|
373
|
+
paths contributed by `monorepo-skills` come later and cannot override any of
|
|
374
|
+
them.
|
|
342
375
|
|
|
343
376
|
## Documentation
|
|
344
377
|
|
package/extensions/ask/index.ts
CHANGED
|
@@ -15,8 +15,8 @@ export const ASK_PROMPT_SNIPPET =
|
|
|
15
15
|
'Ask the user one or more questions with a recommended option, in their language.';
|
|
16
16
|
|
|
17
17
|
export const ASK_PROMPT_GUIDELINES = [
|
|
18
|
-
'When a non-trivial request is not yet a settled spec, follow the sideroom-grill skill before implementing; it
|
|
19
|
-
'A decision the repository already settles consistently is a project fact: read the code and reuse it. A decision that materially affects architecture, dependencies, cost, security, operations, persistence, public contracts, or future change difficulty is an architectural decision: expose it through sideroom_ask before committing, with the practical consequences of each alternative and one recommendation
|
|
18
|
+
'When a non-trivial request is not yet a settled spec, follow the sideroom-grill skill before implementing; it drives sideroom_ask rounds. One sideroom_ask call is one batch; call it again for another round.',
|
|
19
|
+
'A decision the repository already settles consistently is a project fact: read the code and reuse it. A decision that materially affects architecture, dependencies, cost, security, operations, persistence, public contracts, or future change difficulty is an architectural decision: never assume it; follow the sideroom-architecture skill and expose it through sideroom_ask before committing, with the practical consequences of each alternative and one recommendation carrying a one-line justification, never technology names. If the repository shows two inconclusive patterns for the same decision, name the inconsistency in the prompt. Everything else is a minor decision: take it yourself.',
|
|
20
20
|
'Write sideroom_ask prompts, tab labels, option labels, and option descriptions in the language the user is speaking; keep ids and option values in English.',
|
|
21
21
|
'sideroom_ask always adds Out of scope and a write-your-own answer; never send them yourself. A single-selection question needs recommendationIndex; a multiple-selection question needs recommendedIndices and never recommendationIndex.',
|
|
22
22
|
'sideroom_ask only works in the interactive TUI; it errors in print, JSON, and RPC modes.',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const END_OF_OPTIONS = '--';
|
|
2
|
+
const NO_SKILLS_FLAGS = new Set(['--no-skills', '-ns']);
|
|
3
|
+
|
|
4
|
+
export function areSkillsDisabled(argv: readonly string[]): boolean {
|
|
5
|
+
const endOfOptions = argv.indexOf(END_OF_OPTIONS);
|
|
6
|
+
const optionArguments =
|
|
7
|
+
endOfOptions === -1 ? argv : argv.slice(0, endOfOptions);
|
|
8
|
+
return optionArguments.some((argument) => NO_SKILLS_FLAGS.has(argument));
|
|
9
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { areSkillsDisabled } from './flags.ts';
|
|
3
|
+
import { appendMonorepoSkillsNote } from './prompt.ts';
|
|
4
|
+
import { findMonorepoSkillPaths } from './scan.ts';
|
|
5
|
+
|
|
6
|
+
// Pi loads extensions/*/index.ts through export default.
|
|
7
|
+
export default function registerMonorepoSkills(pi: ExtensionAPI): void {
|
|
8
|
+
let hasMonorepoSkills = false;
|
|
9
|
+
|
|
10
|
+
pi.on('resources_discover', (event, ctx) => {
|
|
11
|
+
hasMonorepoSkills = false;
|
|
12
|
+
if (!ctx.isProjectTrusted() || areSkillsDisabled(process.argv)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const skillPaths = findMonorepoSkillPaths(event.cwd);
|
|
17
|
+
if (skillPaths.length === 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
hasMonorepoSkills = true;
|
|
22
|
+
return { skillPaths };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
pi.on('before_agent_start', (event) => {
|
|
26
|
+
if (!hasMonorepoSkills) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const systemPrompt = appendMonorepoSkillsNote(event.systemPrompt);
|
|
31
|
+
if (systemPrompt === event.systemPrompt) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
return { systemPrompt };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const MONOREPO_SKILLS_NOTE_HEADING = 'Sideroom monorepo skills:';
|
|
2
|
+
|
|
3
|
+
export const MONOREPO_SKILLS_NOTE = `${MONOREPO_SKILLS_NOTE_HEADING}
|
|
4
|
+
Some available skills come from child folders of this repository. When skills cover the same work, prefer the skill whose location is inside the folder you are editing.`;
|
|
5
|
+
|
|
6
|
+
export function appendMonorepoSkillsNote(systemPrompt: string): string {
|
|
7
|
+
if (systemPrompt.includes(MONOREPO_SKILLS_NOTE_HEADING)) {
|
|
8
|
+
return systemPrompt;
|
|
9
|
+
}
|
|
10
|
+
if (systemPrompt.length === 0) {
|
|
11
|
+
return MONOREPO_SKILLS_NOTE;
|
|
12
|
+
}
|
|
13
|
+
return `${systemPrompt}\n\n${MONOREPO_SKILLS_NOTE}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import ignore from 'ignore';
|
|
4
|
+
|
|
5
|
+
const MAX_CHILD_DEPTH = 3;
|
|
6
|
+
const IGNORE_FILE_NAMES = ['.gitignore', '.ignore', '.fdignore'] as const;
|
|
7
|
+
const NODE_MODULES_DIRECTORY = 'node_modules';
|
|
8
|
+
const PI_DIRECTORY = '.pi';
|
|
9
|
+
const AGENTS_DIRECTORY = '.agents';
|
|
10
|
+
const SKILLS_DIRECTORY = 'skills';
|
|
11
|
+
const SKILL_FILE_NAME = 'SKILL.md';
|
|
12
|
+
const MARKDOWN_EXTENSION = '.md';
|
|
13
|
+
|
|
14
|
+
export function findMonorepoSkillPaths(cwd: string): string[] {
|
|
15
|
+
const rootDirectory = resolve(cwd);
|
|
16
|
+
const childDirectories = collectChildDirectories(
|
|
17
|
+
rootDirectory,
|
|
18
|
+
rootDirectory,
|
|
19
|
+
0,
|
|
20
|
+
[],
|
|
21
|
+
);
|
|
22
|
+
childDirectories.sort((left, right) =>
|
|
23
|
+
compareText(relative(rootDirectory, left), relative(rootDirectory, right)),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const skillPaths: string[] = [];
|
|
27
|
+
for (const childDirectory of childDirectories) {
|
|
28
|
+
const piSkillsDirectory = join(
|
|
29
|
+
childDirectory,
|
|
30
|
+
PI_DIRECTORY,
|
|
31
|
+
SKILLS_DIRECTORY,
|
|
32
|
+
);
|
|
33
|
+
if (isDirectory(piSkillsDirectory)) {
|
|
34
|
+
skillPaths.push(piSkillsDirectory);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const agentsSkillsDirectory = join(
|
|
38
|
+
childDirectory,
|
|
39
|
+
AGENTS_DIRECTORY,
|
|
40
|
+
SKILLS_DIRECTORY,
|
|
41
|
+
);
|
|
42
|
+
if (isDirectory(agentsSkillsDirectory)) {
|
|
43
|
+
skillPaths.push(...collectAgentsSkillPaths(agentsSkillsDirectory));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return skillPaths;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectChildDirectories(
|
|
50
|
+
directory: string,
|
|
51
|
+
rootDirectory: string,
|
|
52
|
+
depth: number,
|
|
53
|
+
inheritedIgnorePatterns: readonly string[],
|
|
54
|
+
): string[] {
|
|
55
|
+
if (depth >= MAX_CHILD_DEPTH) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ignorePatterns = [
|
|
60
|
+
...inheritedIgnorePatterns,
|
|
61
|
+
...readIgnoreRules(directory, rootDirectory),
|
|
62
|
+
];
|
|
63
|
+
const ignoreMatcher = ignore().add(ignorePatterns);
|
|
64
|
+
const childDirectories: string[] = [];
|
|
65
|
+
for (const entry of readDirectoryEntries(directory)) {
|
|
66
|
+
if (!entry.isDirectory() || isTraversalDirectory(entry.name)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const childDirectory = join(directory, entry.name);
|
|
71
|
+
const relativeChild = toPosixPath(relative(rootDirectory, childDirectory));
|
|
72
|
+
if (ignoreMatcher.ignores(`${relativeChild}/`)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
childDirectories.push(
|
|
77
|
+
childDirectory,
|
|
78
|
+
...collectChildDirectories(
|
|
79
|
+
childDirectory,
|
|
80
|
+
rootDirectory,
|
|
81
|
+
depth + 1,
|
|
82
|
+
ignorePatterns,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return childDirectories;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function collectAgentsSkillPaths(skillsDirectory: string): string[] {
|
|
90
|
+
return collectAgentsSkillPathsFromDirectory(
|
|
91
|
+
skillsDirectory,
|
|
92
|
+
skillsDirectory,
|
|
93
|
+
[],
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function collectAgentsSkillPathsFromDirectory(
|
|
98
|
+
directory: string,
|
|
99
|
+
rootDirectory: string,
|
|
100
|
+
inheritedIgnorePatterns: readonly string[],
|
|
101
|
+
): string[] {
|
|
102
|
+
const ignorePatterns = [
|
|
103
|
+
...inheritedIgnorePatterns,
|
|
104
|
+
...readIgnoreRules(directory, rootDirectory),
|
|
105
|
+
];
|
|
106
|
+
const ignoreMatcher = ignore().add(ignorePatterns);
|
|
107
|
+
const entries = readDirectoryEntries(directory);
|
|
108
|
+
const declaredSkill = entries.find((entry) => {
|
|
109
|
+
if (entry.name !== SKILL_FILE_NAME || !entry.isFile()) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const relativeSkill = toPosixPath(
|
|
113
|
+
relative(rootDirectory, join(directory, entry.name)),
|
|
114
|
+
);
|
|
115
|
+
return !ignoreMatcher.ignores(relativeSkill);
|
|
116
|
+
});
|
|
117
|
+
if (declaredSkill) {
|
|
118
|
+
return [join(directory, declaredSkill.name)];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const skillPaths: string[] = [];
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (isTraversalDirectory(entry.name)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const entryPath = join(directory, entry.name);
|
|
128
|
+
const relativeEntry = toPosixPath(relative(rootDirectory, entryPath));
|
|
129
|
+
if (entry.isFile()) {
|
|
130
|
+
if (
|
|
131
|
+
directory !== rootDirectory &&
|
|
132
|
+
entry.name.endsWith(MARKDOWN_EXTENSION) &&
|
|
133
|
+
!ignoreMatcher.ignores(relativeEntry)
|
|
134
|
+
) {
|
|
135
|
+
skillPaths.push(entryPath);
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!entry.isDirectory() || ignoreMatcher.ignores(`${relativeEntry}/`)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
skillPaths.push(
|
|
143
|
+
...collectAgentsSkillPathsFromDirectory(
|
|
144
|
+
entryPath,
|
|
145
|
+
rootDirectory,
|
|
146
|
+
ignorePatterns,
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return skillPaths;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readIgnoreRules(directory: string, rootDirectory: string): string[] {
|
|
154
|
+
const relativeDirectory = relative(rootDirectory, directory);
|
|
155
|
+
const prefix = relativeDirectory ? `${toPosixPath(relativeDirectory)}/` : '';
|
|
156
|
+
const ignorePatterns: string[] = [];
|
|
157
|
+
|
|
158
|
+
for (const fileName of IGNORE_FILE_NAMES) {
|
|
159
|
+
const ignorePath = join(directory, fileName);
|
|
160
|
+
if (!existsSync(ignorePath)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ignorePatterns.push(
|
|
165
|
+
...readFileSync(ignorePath, 'utf8')
|
|
166
|
+
.split(/\r?\n/)
|
|
167
|
+
.map((line) => prefixIgnorePattern(line, prefix))
|
|
168
|
+
.filter((pattern): pattern is string => pattern !== undefined),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return ignorePatterns;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function prefixIgnorePattern(line: string, prefix: string): string | undefined {
|
|
175
|
+
const trimmed = line.trim();
|
|
176
|
+
if (trimmed.length === 0) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
if (trimmed.startsWith('#') && !trimmed.startsWith('\\#')) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let pattern = line;
|
|
184
|
+
let negated = false;
|
|
185
|
+
if (pattern.startsWith('!')) {
|
|
186
|
+
negated = true;
|
|
187
|
+
pattern = pattern.slice(1);
|
|
188
|
+
} else if (pattern.startsWith('\\!')) {
|
|
189
|
+
pattern = pattern.slice(1);
|
|
190
|
+
}
|
|
191
|
+
if (pattern.startsWith('/')) {
|
|
192
|
+
pattern = pattern.slice(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const prefixed = prefix ? `${prefix}${pattern}` : pattern;
|
|
196
|
+
return negated ? `!${prefixed}` : prefixed;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readDirectoryEntries(directory: string) {
|
|
200
|
+
return readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
|
|
201
|
+
compareText(left.name, right.name),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isDirectory(path: string): boolean {
|
|
206
|
+
return existsSync(path) && lstatSync(path).isDirectory();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isTraversalDirectory(name: string): boolean {
|
|
210
|
+
return name.startsWith('.') || name === NODE_MODULES_DIRECTORY;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function toPosixPath(path: string): string {
|
|
214
|
+
return path.split(sep).join('/');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function compareText(left: string, right: string): number {
|
|
218
|
+
if (left < right) {
|
|
219
|
+
return -1;
|
|
220
|
+
}
|
|
221
|
+
if (left > right) {
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmrdeveloper/sideroom-pi",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.8.0",
|
|
4
4
|
"description": "Pi coding-agent package that asks clarifying questions, shows a live todo board, enforces coding guidelines, and will not call it done while your checks are red.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -94,5 +94,8 @@
|
|
|
94
94
|
"@types/node": "^22.20.1",
|
|
95
95
|
"typebox": "1.3.7",
|
|
96
96
|
"typescript": "^5.6.0"
|
|
97
|
+
},
|
|
98
|
+
"dependencies": {
|
|
99
|
+
"ignore": "7.0.5"
|
|
97
100
|
}
|
|
98
101
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sideroom-architecture
|
|
3
|
+
description: "Surface the architectural decisions a change forces so they are asked, not silently assumed. Use when a feature or change touches module boundaries, data ownership, integration contracts, failure behavior, quality attributes, operations, cost, lock-in, or reversibility, or when the user says the architecture is not settled. Do not use for a trivial or reversible change, a pure glossary lookup, or an already-closed spec."
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: RMRdeveloper
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Sideroom architecture
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
An LLM's default failure is to assume the architecture and start typing. This
|
|
15
|
+
skill forces the opposite: name the architectural decisions a change forces and
|
|
16
|
+
ask the ones that matter, instead of picking them silently. It does not turn
|
|
17
|
+
every change into an interrogation.
|
|
18
|
+
|
|
19
|
+
## Activation Contract
|
|
20
|
+
|
|
21
|
+
Load this skill when a change is about to commit an architectural decision:
|
|
22
|
+
something that materially affects module boundaries, data ownership,
|
|
23
|
+
integration contracts, failure behavior, quality attributes (latency,
|
|
24
|
+
throughput, availability), operations, cost, lock-in, or the difficulty of
|
|
25
|
+
changing course later. It runs under `sideroom-grill` for the architectural
|
|
26
|
+
part of an interview, and on its own when only the architecture is open.
|
|
27
|
+
|
|
28
|
+
Do not load it for a minor decision (reversible, no boundary or contract
|
|
29
|
+
impact), or for a word conflict — that is `sideroom-domain-modeling`.
|
|
30
|
+
|
|
31
|
+
## Hard Rules
|
|
32
|
+
|
|
33
|
+
- Assume nothing architectural. When a change forces an architectural decision
|
|
34
|
+
the code does not already settle, expose it through `sideroom_ask` before
|
|
35
|
+
writing the code. Silent selection of an architectural default is the failure
|
|
36
|
+
this skill exists to prevent.
|
|
37
|
+
- Pick the dimensions this decision actually touches; do not march the whole
|
|
38
|
+
catalog. Two sharp questions beat nine shallow ones. A dimension the change
|
|
39
|
+
does not touch is not asked.
|
|
40
|
+
- Read the code first. A decision the repository already settles consistently
|
|
41
|
+
is a project fact: reuse it, never ask it. When the repository shows two
|
|
42
|
+
inconclusive patterns for the same kind of decision, name that inconsistency
|
|
43
|
+
in the prompt instead of picking one.
|
|
44
|
+
- Quantify or decline. An architectural dimension does not close without a
|
|
45
|
+
number, a range, or an explicit `Out of scope` from the user. A decision
|
|
46
|
+
without a figure is a preference in disguise.
|
|
47
|
+
- Every option of an architectural question carries a `description`: its
|
|
48
|
+
practical consequence, its cost, and the obligation it imposes. The label
|
|
49
|
+
names the choice; the description carries the trade-off. Never a one-word
|
|
50
|
+
technology label.
|
|
51
|
+
- Present the decision as the current situation, the alternatives by their
|
|
52
|
+
practical consequences, and one recommendation with a one-line justification.
|
|
53
|
+
Never reduce it to technology names.
|
|
54
|
+
- One open architectural dimension blocks implementation. `Out of scope` closes
|
|
55
|
+
a thread, is recorded, and is not re-asked.
|
|
56
|
+
|
|
57
|
+
## Dimension catalog
|
|
58
|
+
|
|
59
|
+
Reference, not a checklist. Pick only what the change touches; force a number or
|
|
60
|
+
an explicit `Out of scope` on each one you pick.
|
|
61
|
+
|
|
62
|
+
| Dimension | The question it forces |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Boundaries | Which modules are in, which are out, which dependency direction is forbidden |
|
|
65
|
+
| Data | What is the source of truth, who owns it, how the schema evolves and migrates |
|
|
66
|
+
| Integration | Synchronous or asynchronous, backward compatibility, version window |
|
|
67
|
+
| Quality attributes | Expected latency and volume, availability target — in figures |
|
|
68
|
+
| Failure | What degrades, what retries, what must be idempotent |
|
|
69
|
+
| Operations | How it deploys, how it rolls back, which signal warns it is going wrong |
|
|
70
|
+
| Cost and lock-in | Operating cost, the cost of leaving the provider or dependency |
|
|
71
|
+
| Ownership | Who maintains it, who responds when it breaks |
|
|
72
|
+
| Reversibility | What reopens the decision, and how expensive undoing it is |
|
|
73
|
+
|
|
74
|
+
## Execution Steps
|
|
75
|
+
|
|
76
|
+
1. Read the code around the change. Settle every project fact from the
|
|
77
|
+
codebase, and note any inconsistent pattern to name in a prompt.
|
|
78
|
+
2. List the architectural decisions the change forces. Drop the ones the code
|
|
79
|
+
already settles.
|
|
80
|
+
3. For each remaining decision, pick the dimensions it touches and run one
|
|
81
|
+
`sideroom_ask` round: situation, alternatives by consequence, one
|
|
82
|
+
recommendation, each option with a `description`. Force a number or an
|
|
83
|
+
explicit `Out of scope`.
|
|
84
|
+
4. Repeat rounds until no architectural dimension is open. Do not implement
|
|
85
|
+
with one open.
|
|
86
|
+
5. Record an accepted decision as an ADR under `docs/adr/` only when it clears
|
|
87
|
+
the three `sideroom-domain-modeling` gates: hard to reverse, surprising
|
|
88
|
+
without context, the result of a real trade-off. The ADR names the rejected
|
|
89
|
+
alternatives and what reopens the decision.
|
|
90
|
+
|
|
91
|
+
## Output Contract
|
|
92
|
+
|
|
93
|
+
Before the first round: a one-line statement of what is built, what it touches,
|
|
94
|
+
what is out. After the rounds: the architectural decisions closed with their
|
|
95
|
+
figures, the dimensions marked out of scope, and any ADR written with its
|
|
96
|
+
rejected alternatives and its revision trigger. No architectural default
|
|
97
|
+
reaches the code without having been asked or read from the repository.
|
|
98
|
+
|
|
99
|
+
## References
|
|
100
|
+
|
|
101
|
+
- Driven by `sideroom-grill` for the architectural part of an interview; that
|
|
102
|
+
skill owns the interview, this one owns the architectural interrogation.
|
|
103
|
+
- Hands qualifying decisions to `sideroom-domain-modeling` for the ADR
|
|
104
|
+
three-gate offer and records shared vocabulary through it.
|