@rmrdeveloper/sideroom-pi 8.11.0 → 8.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.
Files changed (64) hide show
  1. package/README.md +49 -5
  2. package/assets/artifacts/GUIDELINES_TEMPLATE.md +3 -3
  3. package/assets/visual-context/browser/INSTALL.md +69 -0
  4. package/assets/visual-context/browser/background.js +578 -0
  5. package/assets/visual-context/browser/content.js +198 -0
  6. package/assets/visual-context/browser/icons/README.md +9 -0
  7. package/assets/visual-context/browser/icons/icon-128.png +0 -0
  8. package/assets/visual-context/browser/icons/icon-16.png +0 -0
  9. package/assets/visual-context/browser/icons/icon-32.png +0 -0
  10. package/assets/visual-context/browser/icons/icon-48.png +0 -0
  11. package/assets/visual-context/browser/manifest.json +20 -0
  12. package/assets/visual-context/browser/popup.css +19 -0
  13. package/assets/visual-context/browser/popup.html +27 -0
  14. package/assets/visual-context/browser/popup.js +141 -0
  15. package/extensions/done/guard.ts +42 -37
  16. package/extensions/done/index.ts +3 -2
  17. package/extensions/done/model.ts +1 -5
  18. package/extensions/explain/guard.ts +13 -11
  19. package/extensions/explain/model.ts +2 -25
  20. package/extensions/guidelines/review.ts +31 -13
  21. package/extensions/jev/client.ts +22 -1
  22. package/extensions/jev/context.ts +36 -3
  23. package/extensions/jev/guard.ts +113 -34
  24. package/extensions/jev/model.ts +90 -12
  25. package/extensions/jev/workflow.ts +188 -0
  26. package/extensions/persona/execute.ts +4 -6
  27. package/extensions/persona/index.ts +3 -2
  28. package/extensions/persona/model.ts +1 -31
  29. package/extensions/persona/prompt.ts +3 -1
  30. package/extensions/shared/settle.ts +63 -13
  31. package/extensions/visual-context/bridge.ts +240 -0
  32. package/extensions/visual-context/broker.ts +292 -0
  33. package/extensions/visual-context/envelope.ts +34 -0
  34. package/extensions/visual-context/index.ts +328 -0
  35. package/extensions/visual-context/literal.ts +34 -0
  36. package/extensions/visual-context/markup.ts +104 -0
  37. package/extensions/visual-context/match.ts +222 -0
  38. package/extensions/visual-context/model.ts +180 -0
  39. package/extensions/visual-context/pairing.ts +114 -0
  40. package/extensions/visual-context/parse-astro.ts +166 -0
  41. package/extensions/visual-context/parse-html.ts +72 -0
  42. package/extensions/visual-context/parse-jsx.ts +157 -0
  43. package/extensions/visual-context/parse-svelte.ts +128 -0
  44. package/extensions/visual-context/parse-vue.ts +133 -0
  45. package/extensions/visual-context/session.ts +192 -0
  46. package/extensions/visual-context/source.ts +197 -0
  47. package/extensions/visual-context/ui.ts +57 -0
  48. package/package.json +16 -7
  49. package/skills/sideroom-architecture/SKILL.md +5 -3
  50. package/skills/sideroom-codebase-design/LICENSE.txt +21 -0
  51. package/skills/sideroom-codebase-design/SKILL.md +151 -0
  52. package/skills/sideroom-codebase-design/references/growth.md +62 -0
  53. package/skills/sideroom-domain-modeling/SKILL.md +1 -1
  54. package/skills/sideroom-domain-scaffold/SKILL.md +6 -4
  55. package/skills/sideroom-grill/SKILL.md +6 -4
  56. package/skills/sideroom-guidelines/SKILL.md +9 -7
  57. package/skills/sideroom-guidelines/references/languages/go.md +3 -3
  58. package/skills/sideroom-guidelines/references/languages/java.md +3 -3
  59. package/skills/sideroom-guidelines/references/languages/javascript.md +25 -3
  60. package/skills/sideroom-guidelines/references/languages/php-laravel.md +3 -3
  61. package/skills/sideroom-guidelines/references/languages/python.md +3 -3
  62. package/skills/sideroom-guidelines/references/languages/rust.md +3 -3
  63. package/skills/sideroom-guidelines/references/languages/typescript.md +27 -3
  64. package/skills/sideroom-persona/SKILL.md +15 -4
@@ -0,0 +1,192 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from '@earendil-works/pi-coding-agent';
5
+ import { type Static, Type } from 'typebox';
6
+ import { Check } from 'typebox/value';
7
+ import {
8
+ addSelection,
9
+ assertSelectionCapacity,
10
+ MAX_CANDIDATES,
11
+ MAX_SELECTIONS,
12
+ type PendingSelection,
13
+ parseSelection,
14
+ SelectionSchema,
15
+ selectionContext,
16
+ VISUAL_SNAPSHOT,
17
+ type VisualSelection,
18
+ } from './model.ts';
19
+ import { candidateIsCurrent, resolveSelection } from './source.ts';
20
+
21
+ const CandidateSchema = Type.Object(
22
+ {
23
+ path: Type.String({
24
+ minLength: 1,
25
+ maxLength: 2_048,
26
+ pattern: '^[^\\x00-\\x1f\\x7f]*$',
27
+ }),
28
+ startLine: Type.Integer({ minimum: 1 }),
29
+ endLine: Type.Integer({ minimum: 1 }),
30
+ fingerprint: Type.String({ pattern: '^[a-f0-9]{64}$' }),
31
+ score: Type.Integer({ minimum: 0 }),
32
+ reasons: Type.Array(Type.String({ maxLength: 200 }), { maxItems: 40 }),
33
+ },
34
+ { additionalProperties: false },
35
+ );
36
+ const SnapshotSchema = Type.Object(
37
+ {
38
+ pending: Type.Array(
39
+ Type.Object(
40
+ {
41
+ selection: SelectionSchema,
42
+ resolution: Type.Object(
43
+ {
44
+ candidates: Type.Array(CandidateSchema, {
45
+ maxItems: MAX_CANDIDATES,
46
+ }),
47
+ truncated: Type.Boolean(),
48
+ capped: Type.Boolean(),
49
+ },
50
+ { additionalProperties: false },
51
+ ),
52
+ chosen: Type.Optional(CandidateSchema),
53
+ },
54
+ { additionalProperties: false },
55
+ ),
56
+ { maxItems: MAX_SELECTIONS },
57
+ ),
58
+ },
59
+ { additionalProperties: false },
60
+ );
61
+
62
+ export interface VisualStore {
63
+ pending: readonly PendingSelection[];
64
+ }
65
+
66
+ export function createVisualStore(): VisualStore {
67
+ return { pending: [] };
68
+ }
69
+
70
+ export function commitPending({
71
+ store,
72
+ pi,
73
+ pending,
74
+ }: {
75
+ readonly store: VisualStore;
76
+ readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
77
+ readonly pending: readonly PendingSelection[];
78
+ }): void {
79
+ store.pending = pending;
80
+ pi.appendEntry(VISUAL_SNAPSHOT, { pending });
81
+ }
82
+
83
+ export function receiveSelection({
84
+ store,
85
+ pi,
86
+ cwd,
87
+ selection,
88
+ }: {
89
+ readonly store: VisualStore;
90
+ readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
91
+ readonly cwd: string;
92
+ readonly selection: VisualSelection;
93
+ }): { readonly candidates: number } {
94
+ const existing = store.pending.find(
95
+ (entry) => entry.selection.id === selection.id,
96
+ );
97
+ if (existing) {
98
+ return { candidates: existing.resolution.candidates.length };
99
+ }
100
+ assertSelectionCapacity(store.pending);
101
+ const resolution = resolveSelection(cwd, selection);
102
+ commitPending({
103
+ store,
104
+ pi,
105
+ pending: addSelection(store.pending, { selection, resolution }),
106
+ });
107
+ return { candidates: resolution.candidates.length };
108
+ }
109
+
110
+ export function restorePending({
111
+ store,
112
+ ctx,
113
+ }: {
114
+ readonly store: VisualStore;
115
+ readonly ctx: Pick<ExtensionContext, 'sessionManager' | 'cwd'>;
116
+ }): void {
117
+ store.pending = [];
118
+ const branch = ctx.sessionManager.getBranch();
119
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
120
+ const entry = branch[index];
121
+ if (entry?.type !== 'custom' || entry.customType !== VISUAL_SNAPSHOT) {
122
+ continue;
123
+ }
124
+ if (!Check(SnapshotSchema, entry.data)) {
125
+ return;
126
+ }
127
+ const snapshot: Static<typeof SnapshotSchema> = entry.data;
128
+ store.pending = snapshot.pending.flatMap((pending) => {
129
+ const candidates = pending.resolution.candidates.filter(
130
+ (candidate) =>
131
+ candidate.startLine <= candidate.endLine &&
132
+ candidateIsCurrent(ctx.cwd, candidate),
133
+ );
134
+ let selection: VisualSelection;
135
+ try {
136
+ selection = parseSelection(pending.selection);
137
+ } catch (cause) {
138
+ throw new Error(
139
+ 'Cannot restore browser context: invalid page address in the session.',
140
+ { cause },
141
+ );
142
+ }
143
+ const chosen = candidates.find(
144
+ (candidate) =>
145
+ candidate.path === pending.chosen?.path &&
146
+ candidate.startLine === pending.chosen.startLine &&
147
+ candidate.fingerprint === pending.chosen.fingerprint,
148
+ );
149
+ return [
150
+ {
151
+ selection,
152
+ resolution: { ...pending.resolution, candidates },
153
+ chosen,
154
+ },
155
+ ];
156
+ });
157
+ return;
158
+ }
159
+ }
160
+
161
+ export function prepareContext({
162
+ store,
163
+ cwd,
164
+ }: {
165
+ readonly store: VisualStore;
166
+ readonly cwd: string;
167
+ }):
168
+ | { readonly text: string; readonly selectionIds: readonly string[] }
169
+ | undefined {
170
+ if (store.pending.length === 0) {
171
+ return undefined;
172
+ }
173
+ const pending = store.pending.map((entry) => {
174
+ const candidates = entry.resolution.candidates.filter((candidate) =>
175
+ candidateIsCurrent(cwd, candidate),
176
+ );
177
+ const chosen =
178
+ entry.chosen && candidates.includes(entry.chosen)
179
+ ? entry.chosen
180
+ : undefined;
181
+ return {
182
+ ...entry,
183
+ resolution: { ...entry.resolution, candidates },
184
+ chosen,
185
+ };
186
+ });
187
+ const context = selectionContext(pending);
188
+ return {
189
+ text: context,
190
+ selectionIds: pending.map((entry) => entry.selection.id),
191
+ };
192
+ }
@@ -0,0 +1,197 @@
1
+ import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { extname, join, relative, sep } from 'node:path';
3
+ import ignore from 'ignore';
4
+ import { projectRelativePath } from '../shared/file-path.ts';
5
+ import { isMissingFile } from '../shared/read-file.ts';
6
+ import { InvalidMarkup } from './markup.ts';
7
+ import { matchSource, sourceFingerprint } from './match.ts';
8
+ import {
9
+ MAX_CANDIDATES,
10
+ type SourceCandidate,
11
+ type SourceResolution,
12
+ type VisualSelection,
13
+ } from './model.ts';
14
+
15
+ const SOURCE_EXTENSIONS = new Set([
16
+ '.html',
17
+ '.htm',
18
+ '.vue',
19
+ '.svelte',
20
+ '.astro',
21
+ '.jsx',
22
+ '.tsx',
23
+ ]);
24
+ const SKIP_DIRECTORIES = new Set([
25
+ 'node_modules',
26
+ 'dist',
27
+ 'build',
28
+ 'coverage',
29
+ 'vendor',
30
+ ]);
31
+ const IGNORE_FILES = ['.gitignore', '.ignore', '.fdignore'];
32
+ const MAX_ENTRIES = 4_096;
33
+ const MAX_FILE_BYTES = 262_144;
34
+ const MAX_SOURCE_BYTES = 8_388_608;
35
+ const MAX_DEPTH = 16;
36
+ const SENSITIVE_NAME =
37
+ /secret|credential|password|private.?key|(?:^|[._-])env(?:[._-]|$)/iu;
38
+
39
+ export function resolveSelection(
40
+ cwd: string,
41
+ selection: VisualSelection,
42
+ ): SourceResolution {
43
+ const root = realpathSync(cwd);
44
+ const candidates: SourceCandidate[] = [];
45
+ let entriesRead = 0;
46
+ let sourceBytes = 0;
47
+ let truncated = false;
48
+ function walk({
49
+ directory,
50
+ patterns,
51
+ depth,
52
+ }: {
53
+ readonly directory: string;
54
+ readonly patterns: readonly string[];
55
+ readonly depth: number;
56
+ }): void {
57
+ if (depth > MAX_DEPTH) {
58
+ truncated = true;
59
+ return;
60
+ }
61
+ const localPatterns = [...patterns, ...ignorePatterns(directory, root)];
62
+ const matcher = ignore().add(localPatterns);
63
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort(
64
+ (left, right) => left.name.localeCompare(right.name),
65
+ )) {
66
+ entriesRead += 1;
67
+ if (entriesRead > MAX_ENTRIES || sourceBytes >= MAX_SOURCE_BYTES) {
68
+ truncated = true;
69
+ return;
70
+ }
71
+ if (
72
+ entry.name.startsWith('.') ||
73
+ [...entry.name].some(
74
+ (character) =>
75
+ character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
76
+ ) ||
77
+ SENSITIVE_NAME.test(entry.name) ||
78
+ entry.isSymbolicLink()
79
+ ) {
80
+ continue;
81
+ }
82
+ const absolutePath = join(directory, entry.name);
83
+ const path = relative(root, absolutePath).split(sep).join('/');
84
+ if (matcher.ignores(entry.isDirectory() ? `${path}/` : path)) {
85
+ continue;
86
+ }
87
+ if (entry.isDirectory()) {
88
+ if (!SKIP_DIRECTORIES.has(entry.name)) {
89
+ walk({
90
+ directory: absolutePath,
91
+ patterns: localPatterns,
92
+ depth: depth + 1,
93
+ });
94
+ }
95
+ continue;
96
+ }
97
+ if (
98
+ !entry.isFile() ||
99
+ !SOURCE_EXTENSIONS.has(extname(entry.name).toLowerCase())
100
+ ) {
101
+ continue;
102
+ }
103
+ const size = statSync(absolutePath).size;
104
+ if (size > MAX_FILE_BYTES || sourceBytes + size > MAX_SOURCE_BYTES) {
105
+ truncated = true;
106
+ continue;
107
+ }
108
+ sourceBytes += size;
109
+ const source = readFileSync(absolutePath, 'utf8');
110
+ try {
111
+ candidates.push(...matchSource({ path, source, selection }));
112
+ } catch (cause) {
113
+ if (cause instanceof InvalidMarkup) {
114
+ truncated = true;
115
+ continue;
116
+ }
117
+ throw new Error(`Cannot inspect visual source ${path}`, { cause });
118
+ }
119
+ }
120
+ }
121
+ walk({ directory: root, patterns: [], depth: 0 });
122
+ candidates.sort(
123
+ (left, right) =>
124
+ right.score - left.score ||
125
+ left.path.localeCompare(right.path) ||
126
+ left.startLine - right.startLine,
127
+ );
128
+ return {
129
+ candidates: candidates.slice(0, MAX_CANDIDATES),
130
+ truncated,
131
+ capped: candidates.length > MAX_CANDIDATES,
132
+ };
133
+ }
134
+
135
+ function ignorePatterns(directory: string, root: string): readonly string[] {
136
+ const prefix = relative(root, directory).split(sep).join('/');
137
+ const patterns: string[] = [];
138
+ for (const file of IGNORE_FILES) {
139
+ let source: string;
140
+ try {
141
+ source = readFileSync(join(directory, file), 'utf8');
142
+ } catch (cause) {
143
+ if (isMissingFile(cause)) {
144
+ continue;
145
+ }
146
+ throw new Error(`Cannot read ignore file ${file}`, { cause });
147
+ }
148
+ for (const line of source.split(/\r?\n/u)) {
149
+ if (!line || line.startsWith('#')) {
150
+ continue;
151
+ }
152
+ const negated = line.startsWith('!');
153
+ const pattern = negated ? line.slice(1) : line;
154
+ const nested = prefix
155
+ ? `${prefix}/${pattern.startsWith('/') ? pattern.slice(1) : pattern.includes('/') ? pattern : `**/${pattern}`}`
156
+ : pattern;
157
+ patterns.push(`${negated ? '!' : ''}${nested}`);
158
+ }
159
+ }
160
+ return patterns;
161
+ }
162
+
163
+ export function candidateIsCurrent(
164
+ cwd: string,
165
+ candidate: SourceCandidate,
166
+ ): boolean {
167
+ try {
168
+ const absolutePath = realpathSync(join(cwd, candidate.path));
169
+ if (
170
+ projectRelativePath(cwd, absolutePath) === undefined ||
171
+ statSync(absolutePath).size > MAX_FILE_BYTES
172
+ ) {
173
+ return false;
174
+ }
175
+ const source = readFileSync(absolutePath, 'utf8');
176
+ return (
177
+ sourceFingerprint(source) === candidate.fingerprint &&
178
+ candidate.endLine <= source.split('\n').length
179
+ );
180
+ } catch (cause) {
181
+ if (isGone(cause)) {
182
+ return false;
183
+ }
184
+ throw new Error(`Cannot verify selected source ${candidate.path}`, {
185
+ cause,
186
+ });
187
+ }
188
+ }
189
+
190
+ // A path that turned into a file on the way reports ENOTDIR, which means the
191
+ // remembered source is gone rather than unreadable.
192
+ function isGone(cause: unknown): boolean {
193
+ return (
194
+ isMissingFile(cause) ||
195
+ (cause instanceof Error && 'code' in cause && cause.code === 'ENOTDIR')
196
+ );
197
+ }
@@ -0,0 +1,57 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';
3
+ import {
4
+ candidateLabel,
5
+ elementLabel,
6
+ type PendingSelection,
7
+ VISUAL_WIDGET,
8
+ } from './model.ts';
9
+
10
+ export const BROWSER_EXTENSION_DIRECTORY = fileURLToPath(
11
+ new URL('../../assets/visual-context/browser/', import.meta.url),
12
+ );
13
+
14
+ function pendingLabel({ resolution, chosen }: PendingSelection): string {
15
+ if (chosen) {
16
+ return `${candidateLabel(chosen)} (selected candidate)`;
17
+ }
18
+ const truncated = resolution.truncated ? ' · search truncated' : '';
19
+ const capped = resolution.capped ? ' · more candidates' : '';
20
+ return `${String(resolution.candidates.length)} candidates${truncated}${capped}`;
21
+ }
22
+
23
+ export function renderVisualWidget(
24
+ pending: readonly PendingSelection[],
25
+ theme: Theme,
26
+ ): readonly string[] {
27
+ return [
28
+ '',
29
+ theme.fg(
30
+ 'dim',
31
+ `Browser elements (${String(pending.length)}) · F7: review`,
32
+ ),
33
+ ...pending.map((entry) =>
34
+ theme.fg(
35
+ entry.chosen ? 'accent' : 'muted',
36
+ `${elementLabel(entry.selection)} — ${pendingLabel(entry)}`,
37
+ ),
38
+ ),
39
+ ];
40
+ }
41
+
42
+ export function refreshVisualWidget(
43
+ pending: readonly PendingSelection[],
44
+ ctx: Pick<ExtensionContext, 'ui' | 'mode'>,
45
+ ): void {
46
+ if (ctx.mode !== 'tui') {
47
+ return;
48
+ }
49
+ if (pending.length === 0) {
50
+ ctx.ui.setWidget(VISUAL_WIDGET, undefined);
51
+ return;
52
+ }
53
+ ctx.ui.setWidget(VISUAL_WIDGET, (_tui, theme) => ({
54
+ render: () => [...renderVisualWidget(pending, theme)],
55
+ invalidate() {},
56
+ }));
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmrdeveloper/sideroom-pi",
3
- "version": "8.11.0",
3
+ "version": "8.13.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": [
@@ -17,14 +17,15 @@
17
17
  "commitlint": "commitlint",
18
18
  "types": "tsc --noEmit",
19
19
  "test": "node --test 'extensions/**/*.test.ts' 'scripts/**/*.test.mjs'",
20
- "lint": "biome lint extensions scripts commitlint.config.mjs",
21
- "lint:fix": "biome lint --write extensions scripts commitlint.config.mjs",
22
- "format": "biome format --write extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
23
- "format:check": "biome format extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
20
+ "lint": "biome lint extensions assets/visual-context/browser scripts commitlint.config.mjs",
21
+ "lint:fix": "biome lint --write extensions assets/visual-context/browser scripts commitlint.config.mjs",
22
+ "format": "biome format --write extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
23
+ "format:check": "biome format extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
24
24
  "preview": "node scripts/render-preview.mjs",
25
25
  "cache:report": "node scripts/cache-report.mjs",
26
+ "package:browser": "node scripts/package-browser.mjs",
26
27
  "check:if-blocks": "node scripts/check-if-blocks.mjs",
27
- "check": "npm run types && npm run check:if-blocks && biome check extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json && npm test"
28
+ "check": "npm run types && npm run check:if-blocks && biome check extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json && npm test"
28
29
  },
29
30
  "engines": {
30
31
  "node": ">=22.19.0"
@@ -92,10 +93,18 @@
92
93
  "@earendil-works/pi-coding-agent": "0.87.1",
93
94
  "@earendil-works/pi-tui": "0.87.1",
94
95
  "@types/node": "^22.20.1",
96
+ "@types/ws": "^8.18.2",
97
+ "fflate": "0.8.3",
95
98
  "typebox": "1.3.7",
96
99
  "typescript": "^5.6.0"
97
100
  },
98
101
  "dependencies": {
99
- "ignore": "7.0.5"
102
+ "@astrojs/compiler": "^4.0.0",
103
+ "@babel/parser": "^7.29.9",
104
+ "@vue/compiler-dom": "^3.5.43",
105
+ "ignore": "7.0.5",
106
+ "parse5": "^8.0.1",
107
+ "svelte": "^5.57.2",
108
+ "ws": "^8.22.0"
100
109
  }
101
110
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
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."
3
+ description: "Resolve architectural choices before code: module boundaries, data ownership, API or integration contracts, persistence, failure and retry behavior, deployment, capacity, cost, or provider choice. Use when designing or changing these and the repository or user has not settled the choice, even if the feature requirements are clear. Skip routine changes that reuse established contracts; use sideroom-grill when the broader plan is also open."
4
4
  license: MIT
5
5
  metadata:
6
6
  author: RMRdeveloper
@@ -25,8 +25,10 @@ throughput, availability), operations, cost, lock-in, or the difficulty of
25
25
  changing course later. It runs under `sideroom-grill` for the architectural
26
26
  part of an interview, and on its own when only the architecture is open.
27
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`.
28
+ Clear feature requirements do not settle the architecture by themselves.
29
+ Load this skill if a consequential choice remains, even when it is reversible.
30
+ Do not load it for routine changes that reuse established boundaries and
31
+ contracts, or for a word conflict — that is `sideroom-domain-modeling`.
30
32
 
31
33
  ## Hard Rules
32
34
 
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Pocock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.