@bompus/house-rules 0.0.0-stage → 0.11.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 (191) hide show
  1. package/CONTRIBUTING.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +347 -2
  4. package/THIRD_PARTY_NOTICES.md +188 -0
  5. package/compose.mjs +119 -0
  6. package/composition.mjs +353 -0
  7. package/config-view.mjs +194 -0
  8. package/config.mjs +320 -0
  9. package/docs/assets/banner-dark.png +0 -0
  10. package/docs/assets/banner-light.png +0 -0
  11. package/docs/assets/overview.png +0 -0
  12. package/docs/configuration.md +193 -0
  13. package/docs/npm-release.md +45 -0
  14. package/docs/skill-names.md +50 -0
  15. package/examples/person/house-rules.json +15 -0
  16. package/examples/person/rules/10-tooling.md +8 -0
  17. package/examples/person/rules/20-reporting.md +9 -0
  18. package/examples/project/AGENTS.md +18 -0
  19. package/package.json +40 -4
  20. package/rule-references.mjs +91 -0
  21. package/rules/core.md +431 -0
  22. package/rules/modifiers/coded-offers.md +40 -0
  23. package/rules/modifiers/effort-estimates.md +41 -0
  24. package/rules/modifiers/land-when-done.md +12 -0
  25. package/rules/modifiers/low-quota-handoff.md +19 -0
  26. package/rules/modifiers/multi-agent.md +34 -0
  27. package/rules/modifiers/no-attribution.md +10 -0
  28. package/rules/modifiers/plan-files.md +50 -0
  29. package/rules/modifiers/question-cards.md +48 -0
  30. package/rules/modifiers/release-batching.md +13 -0
  31. package/rules/modifiers/scratch-on-disk.md +15 -0
  32. package/rules/modifiers/shared-host-load.md +48 -0
  33. package/rules/modifiers/solo-operator.md +13 -0
  34. package/rules/modifiers/squash-landing.md +22 -0
  35. package/rules/modifiers/swarmail.md +12 -0
  36. package/rules/references/github-actions.md +45 -0
  37. package/rules/references/landing.md +39 -0
  38. package/rules/references/multi-agent.md +47 -0
  39. package/rules/references/release-batching.md +43 -0
  40. package/rules/references/squash-landing.md +44 -0
  41. package/setup.mjs +375 -0
  42. package/skills/hr-agent-guidance-audit/LICENSE +21 -0
  43. package/skills/hr-agent-guidance-audit/SKILL.md +86 -0
  44. package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
  45. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
  46. package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
  47. package/skills/hr-agent-guidance-refresh/SKILL.md +68 -0
  48. package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
  49. package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
  50. package/skills/hr-api-exposure-check/SKILL.md +159 -0
  51. package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
  52. package/skills/hr-audit-choices/SKILL.md +67 -0
  53. package/skills/hr-audit-choices/agents/openai.yaml +2 -0
  54. package/skills/hr-benchmarking/NOTICE.md +36 -0
  55. package/skills/hr-benchmarking/SKILL.md +316 -0
  56. package/skills/hr-benchmarking/agents/openai.yaml +2 -0
  57. package/skills/hr-benchmarking/references/javascript.md +17 -0
  58. package/skills/hr-benchmarking/references/priority-findings-20261006.md +115 -0
  59. package/skills/hr-benchmarking/references/tools.md +160 -0
  60. package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
  61. package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
  62. package/skills/hr-better-accessibility/LICENSE +22 -0
  63. package/skills/hr-better-accessibility/NOTICE.md +46 -0
  64. package/skills/hr-better-accessibility/SKILL.md +119 -0
  65. package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
  66. package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
  67. package/skills/hr-better-accessibility/forms.md +82 -0
  68. package/skills/hr-better-accessibility/hit-areas.md +98 -0
  69. package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
  70. package/skills/hr-better-accessibility/screen-readers.md +101 -0
  71. package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
  72. package/skills/hr-better-colors/LICENSE +21 -0
  73. package/skills/hr-better-colors/NOTICE.md +37 -0
  74. package/skills/hr-better-colors/SKILL.md +114 -0
  75. package/skills/hr-better-colors/agents/openai.yaml +3 -0
  76. package/skills/hr-better-colors/color-formats.md +90 -0
  77. package/skills/hr-better-colors/color-usage.md +140 -0
  78. package/skills/hr-better-colors/contrast.md +80 -0
  79. package/skills/hr-better-colors/palette-generation.md +104 -0
  80. package/skills/hr-better-colors/palette-structure.md +76 -0
  81. package/skills/hr-better-colors/token-naming.md +97 -0
  82. package/skills/hr-better-layout/LICENSE +22 -0
  83. package/skills/hr-better-layout/NOTICE.md +48 -0
  84. package/skills/hr-better-layout/SKILL.md +92 -0
  85. package/skills/hr-better-layout/agents/openai.yaml +3 -0
  86. package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
  87. package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
  88. package/skills/hr-better-typography/LICENSE +22 -0
  89. package/skills/hr-better-typography/NOTICE.md +46 -0
  90. package/skills/hr-better-typography/SKILL.md +172 -0
  91. package/skills/hr-better-typography/agents/openai.yaml +3 -0
  92. package/skills/hr-better-typography/choosing-fonts.md +69 -0
  93. package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
  94. package/skills/hr-better-typography/details-and-accessibility.md +119 -0
  95. package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
  96. package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
  97. package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
  98. package/skills/hr-better-writing/LICENSE +22 -0
  99. package/skills/hr-better-writing/NOTICE.md +46 -0
  100. package/skills/hr-better-writing/SKILL.md +122 -0
  101. package/skills/hr-better-writing/agents/openai.yaml +3 -0
  102. package/skills/hr-change-impact/SKILL.md +113 -0
  103. package/skills/hr-change-impact/agents/openai.yaml +5 -0
  104. package/skills/hr-code-review/LICENSE +21 -0
  105. package/skills/hr-code-review/NOTICE.md +26 -0
  106. package/skills/hr-code-review/SKILL.md +147 -0
  107. package/skills/hr-code-review/agents/openai.yaml +3 -0
  108. package/skills/hr-code-review/references/evidence.md +57 -0
  109. package/skills/hr-design-exploration/SKILL.md +82 -0
  110. package/skills/hr-design-exploration/agents/openai.yaml +4 -0
  111. package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
  112. package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
  113. package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
  114. package/skills/hr-diagnosing-bugs/references/reporting.md +72 -0
  115. package/skills/hr-explain-code/NOTICE.md +37 -0
  116. package/skills/hr-explain-code/SKILL.md +110 -0
  117. package/skills/hr-explain-code/agents/openai.yaml +5 -0
  118. package/skills/hr-explain-code/references/helper-brief.md +39 -0
  119. package/skills/hr-extract-shared-steps/SKILL.md +134 -0
  120. package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
  121. package/skills/hr-handoff/SKILL.md +91 -0
  122. package/skills/hr-handoff/agents/openai.yaml +5 -0
  123. package/skills/hr-handoff/assets/ledger.example.json +10 -0
  124. package/skills/hr-handoff/assets/receipt.example.json +76 -0
  125. package/skills/hr-handoff/assets/source.example.json +20 -0
  126. package/skills/hr-handoff/references/reconciliation.md +105 -0
  127. package/skills/hr-handoff/scripts/check-reconciliation.mjs +282 -0
  128. package/skills/hr-house-rules-setup/SKILL.md +100 -0
  129. package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
  130. package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
  131. package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
  132. package/skills/hr-lean-plan/SKILL.md +142 -0
  133. package/skills/hr-lean-plan/agents/openai.yaml +5 -0
  134. package/skills/hr-maintainability-review/SKILL.md +176 -0
  135. package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
  136. package/skills/hr-manual-qa/NOTICE.md +36 -0
  137. package/skills/hr-manual-qa/SKILL.md +37 -0
  138. package/skills/hr-manual-qa/agents/openai.yaml +3 -0
  139. package/skills/hr-navigation-retro/LICENSE +21 -0
  140. package/skills/hr-navigation-retro/NOTICE.md +5 -0
  141. package/skills/hr-navigation-retro/SKILL.md +86 -0
  142. package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
  143. package/skills/hr-navigation-retro/references/inputs.md +67 -0
  144. package/skills/hr-navigation-retro/scripts/retro.py +552 -0
  145. package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
  146. package/skills/hr-ordering-tests/SKILL.md +51 -0
  147. package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
  148. package/skills/hr-plain-prose/SKILL.md +261 -0
  149. package/skills/hr-plain-prose/agents/openai.yaml +3 -0
  150. package/skills/hr-plain-prose/scripts/check.mjs +331 -0
  151. package/skills/hr-pr-followup/LICENSE +21 -0
  152. package/skills/hr-pr-followup/NOTICE.md +9 -0
  153. package/skills/hr-pr-followup/SKILL.md +71 -0
  154. package/skills/hr-pr-followup/agents/openai.yaml +2 -0
  155. package/skills/hr-pr-followup/references/review-helpers.md +90 -0
  156. package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
  157. package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
  158. package/skills/hr-progress-report/SKILL.md +49 -0
  159. package/skills/hr-progress-report/agents/openai.yaml +5 -0
  160. package/skills/hr-read-reddit/SKILL.md +44 -0
  161. package/skills/hr-read-reddit/agents/openai.yaml +2 -0
  162. package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
  163. package/skills/hr-read-x-links/SKILL.md +42 -0
  164. package/skills/hr-read-x-links/agents/openai.yaml +2 -0
  165. package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
  166. package/skills/hr-split-to-prs/LICENSE +21 -0
  167. package/skills/hr-split-to-prs/NOTICE.md +9 -0
  168. package/skills/hr-split-to-prs/SKILL.md +61 -0
  169. package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
  170. package/skills/hr-stock-ui-audit/SKILL.md +132 -0
  171. package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
  172. package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
  173. package/skills/hr-tdd/LICENSE +21 -0
  174. package/skills/hr-tdd/NOTICE.md +10 -0
  175. package/skills/hr-tdd/SKILL.md +40 -0
  176. package/skills/hr-tdd/agents/openai.yaml +3 -0
  177. package/skills/hr-tdd/mocking.md +39 -0
  178. package/skills/hr-tdd/tests.md +77 -0
  179. package/skills/hr-test-audit/SKILL.md +146 -0
  180. package/skills/hr-test-audit/agents/openai.yaml +3 -0
  181. package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
  182. package/skills/hr-what-next/SKILL.md +61 -0
  183. package/skills/hr-what-next/agents/openai.yaml +4 -0
  184. package/skills/hr-what-next/references/reporting.md +68 -0
  185. package/skills/hr-writing-for-agents/NOTICE.md +46 -0
  186. package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
  187. package/skills/hr-writing-for-agents/SKILL.md +108 -0
  188. package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
  189. package/skills/hr-writing-pr/NOTICE.md +48 -0
  190. package/skills/hr-writing-pr/SKILL.md +104 -0
  191. package/skills/hr-writing-pr/agents/openai.yaml +2 -0
package/config.mjs ADDED
@@ -0,0 +1,320 @@
1
+ // Selection management for compose.mjs config. JSON is the only settings store.
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import {
4
+ closeSync,
5
+ existsSync,
6
+ fsyncSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ realpathSync,
12
+ renameSync,
13
+ statSync,
14
+ unlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { basename, dirname, join, relative, resolve } from "node:path";
18
+ import { parseArgs } from "node:util";
19
+ import { fileURLToPath } from "node:url";
20
+ const SHIPPED_SKILLS = join(dirname(fileURLToPath(import.meta.url)), "skills");
21
+ import {
22
+ composeConfiguration,
23
+ configurationPath,
24
+ modifierList,
25
+ parseFragment,
26
+ skillSources,
27
+ splitSections,
28
+ } from "./composition.mjs";
29
+ import { renderConfig, renderHelp } from "./config-view.mjs";
30
+
31
+ const COMMANDS = ["catalog", "status", "validate", "preview", "set"];
32
+ const TOGGLES = ["enable-modifier", "disable-modifier", "enable-skill", "disable-skill"];
33
+ const revisionOf = (text) =>
34
+ text === null ? "missing" : createHash("sha256").update(text).digest("hex");
35
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
36
+
37
+ function stringList(value, key) {
38
+ if (value === undefined) return;
39
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string" || !v.trim()))
40
+ throw new Error(`${key} must be an array of non-empty strings`);
41
+ if (new Set(value).size !== value.length) throw new Error(`${key} contains duplicates`);
42
+ }
43
+
44
+ function validateStructure(config) {
45
+ if (!object(config)) throw new Error("configuration must be a JSON object");
46
+ for (const key of ["modifiers", "layers"]) stringList(config[key], key);
47
+ if (config.skills !== undefined && !object(config.skills))
48
+ throw new Error("skills must be an object");
49
+ for (const key of ["include", "exclude", "independent"])
50
+ stringList(config.skills?.[key], `skills.${key}`);
51
+ }
52
+
53
+ function validateSelection(config) {
54
+ validateStructure(config);
55
+ const known = new Set(modifierList().map((m) => m.name));
56
+ for (const name of config.modifiers ?? []) {
57
+ if (!known.has(name)) throw new Error(`unknown modifier "${name}" (see config catalog)`);
58
+ }
59
+ if (config.modifiers?.includes("question-cards") && !config.modifiers.includes("coded-offers"))
60
+ throw new Error(
61
+ "question-cards requires coded-offers; use --questions cards or disable question-cards",
62
+ );
63
+ }
64
+
65
+ export function readConfiguration(path) {
66
+ // Preserve the caller's config directory: relative layers use the same base as composition.
67
+ path = resolve(path);
68
+ let text = null;
69
+ try {
70
+ if (!lstatSync(path).isFile())
71
+ throw new Error("config must be a regular file, not a symlink or directory");
72
+ text = readFileSync(path, "utf8");
73
+ } catch (e) {
74
+ if (e.code !== "ENOENT") throw e;
75
+ }
76
+ let config = {};
77
+ try {
78
+ if (text !== null) config = JSON.parse(text.replace(/^\uFEFF/, ""));
79
+ } catch {
80
+ throw new Error("invalid JSON in configuration");
81
+ }
82
+ validateStructure(config);
83
+ return { path, text, revision: revisionOf(text), config };
84
+ }
85
+
86
+ export function selectionReport(config, path) {
87
+ validateSelection(config);
88
+ const result = composeConfiguration(config, dirname(path));
89
+ const allSkills = skillSources(
90
+ { ...config, skills: { ...config.skills, include: undefined, exclude: [] } },
91
+ dirname(path),
92
+ );
93
+ const selected = new Set(config.modifiers ?? []);
94
+ const modifiers = modifierList().map((m) => ({ ...m, enabled: selected.has(m.name) }));
95
+ const skills = [...allSkills].map(([name, source]) => ({
96
+ name,
97
+ source,
98
+ origin: source === join(SHIPPED_SKILLS, name) ? "shipped" : relative(dirname(path), source),
99
+ enabled: result.skills.has(name),
100
+ explicitOnly:
101
+ parseFragment(readFileSync(join(source, "SKILL.md"), "utf8"), source).meta[
102
+ "disable-model-invocation"
103
+ ] === "true",
104
+ description:
105
+ parseFragment(readFileSync(join(source, "SKILL.md"), "utf8"), source).meta.description ?? "",
106
+ }));
107
+ const unavailable = (config.skills?.exclude ?? []).filter((name) => !allSkills.has(name));
108
+ const rules = splitSections(result.rules).sections.map((s) => ({
109
+ heading: s.heading,
110
+ source: "core",
111
+ }));
112
+ const origins = new Map();
113
+ for (const f of result.fragments) {
114
+ for (const section of splitSections(f.body).sections) origins.set(section.heading, f.source);
115
+ }
116
+ for (const rule of rules) {
117
+ rule.source = origins.get(rule.heading) ?? "core";
118
+ rule.origin =
119
+ rule.source === "core" || rule.source.startsWith("modifier ")
120
+ ? rule.source
121
+ : relative(dirname(path), rule.source);
122
+ }
123
+ return {
124
+ modifiers,
125
+ skills,
126
+ rules,
127
+ unavailable,
128
+ layers: (config.layers ?? ["."]).map((layer) => resolve(dirname(path), layer)),
129
+ fragments: result.fragments.map((f) => ({ source: f.source, ...f.meta })),
130
+ questions: selected.has("question-cards")
131
+ ? "cards"
132
+ : selected.has("coded-offers")
133
+ ? "coded"
134
+ : "plain",
135
+ warnings: result.warnings,
136
+ composedRules: result.rules,
137
+ hostLoading: "unverified",
138
+ };
139
+ }
140
+
141
+ export function changeSelection(config, options, path) {
142
+ const next = structuredClone(config);
143
+ const modifiers = new Set(next.modifiers ?? []);
144
+ const excluded = new Set(next.skills?.exclude ?? []);
145
+ const included = next.skills?.include === undefined ? null : new Set(next.skills.include);
146
+ const knownModifiers = new Set(modifierList().map((m) => m.name));
147
+ // Use discovery even for excluded skills; preserve unrelated existing exclusions.
148
+ const knownSkills = new Set(
149
+ skillSources(
150
+ { ...next, skills: { ...next.skills, include: undefined, exclude: [] } },
151
+ dirname(path),
152
+ ).keys(),
153
+ );
154
+ for (const [kind, known, choices] of [
155
+ ["modifier", knownModifiers, modifiers],
156
+ ["skill", knownSkills, excluded],
157
+ ]) {
158
+ const enable = options[`enable-${kind}`] ?? [];
159
+ const disable = options[`disable-${kind}`] ?? [];
160
+ for (const name of [...enable, ...disable]) {
161
+ if (!known.has(name) && !(kind === "modifier" && disable.includes(name) && choices.has(name)))
162
+ throw new Error(`unknown ${kind} "${name}" (see config catalog)`);
163
+ if (enable.includes(name) && disable.includes(name))
164
+ throw new Error(`cannot enable and disable "${name}" together`);
165
+ if (
166
+ kind === "modifier" &&
167
+ options.questions !== undefined &&
168
+ ["coded-offers", "question-cards"].includes(name)
169
+ )
170
+ throw new Error("use --questions or explicit question modifiers, not both");
171
+ }
172
+ for (const name of enable) {
173
+ if (kind === "modifier") choices.add(name);
174
+ else {
175
+ choices.delete(name);
176
+ if (included && existsSync(join(SHIPPED_SKILLS, name, "SKILL.md"))) included.add(name);
177
+ }
178
+ }
179
+ for (const name of disable) {
180
+ if (kind === "modifier") choices.delete(name);
181
+ else choices.add(name);
182
+ }
183
+ }
184
+ if (options.questions !== undefined) {
185
+ if (!["plain", "coded", "cards"].includes(options.questions))
186
+ throw new Error("--questions must be plain, coded or cards");
187
+ if (options.questions === "plain") modifiers.delete("coded-offers");
188
+ else modifiers.add("coded-offers");
189
+ if (options.questions === "cards") modifiers.add("question-cards");
190
+ else modifiers.delete("question-cards");
191
+ }
192
+ if (JSON.stringify([...modifiers]) !== JSON.stringify(next.modifiers ?? []))
193
+ next.modifiers = [...modifiers];
194
+ if (JSON.stringify([...excluded]) !== JSON.stringify(next.skills?.exclude ?? []))
195
+ next.skills = { ...next.skills, exclude: [...excluded] };
196
+ if (included) next.skills.include = [...included];
197
+ selectionReport(next, path); // Composition and legacy-name errors precede every write.
198
+ return next;
199
+ }
200
+
201
+ // Cooperating CLI writers share an exclusive lock; expected revision guards stale previews.
202
+ // Manual editors do not share this lock. Recheck before rename, but they can still race that rename.
203
+ export function writeConfiguration(snapshot, next, expected) {
204
+ if (!expected) throw new Error("--apply requires --expect <revision> from status or a preview");
205
+ selectionReport(next, snapshot.path);
206
+ mkdirSync(dirname(snapshot.path), { recursive: true });
207
+ // Lock aliases of the same parent together without changing relative layer discovery.
208
+ const lock = `${join(realpathSync(dirname(snapshot.path)), basename(snapshot.path))}.lock`;
209
+ let lockFd;
210
+ try {
211
+ lockFd = openSync(lock, "wx", 0o600);
212
+ } catch (e) {
213
+ if (e.code === "EEXIST")
214
+ throw new Error(
215
+ `configuration is locked: ${lock}; inspect the owner before removing an abandoned lock`,
216
+ );
217
+ throw e;
218
+ }
219
+ const temporary = `${snapshot.path}.${randomUUID()}.tmp`;
220
+ try {
221
+ writeFileSync(lockFd, JSON.stringify({ pid: process.pid }));
222
+ const current = readConfiguration(snapshot.path);
223
+ if (current.revision !== expected || current.revision !== snapshot.revision)
224
+ throw new Error("configuration changed since preview; read status and preview again");
225
+ if (current.text !== null && JSON.stringify(current.config) === JSON.stringify(next))
226
+ return { changed: false, revision: current.revision };
227
+ const text = `${JSON.stringify(next, null, 2)}\n`;
228
+ const fd = openSync(
229
+ temporary,
230
+ "wx",
231
+ current.text === null ? 0o600 : statSync(current.path).mode & 0o777,
232
+ );
233
+ try {
234
+ writeFileSync(fd, text);
235
+ fsyncSync(fd);
236
+ } finally {
237
+ closeSync(fd);
238
+ }
239
+ if (readConfiguration(snapshot.path).revision !== current.revision)
240
+ throw new Error("configuration changed during validation; preview again");
241
+ renameSync(temporary, snapshot.path);
242
+ return { changed: true, revision: revisionOf(text) };
243
+ } finally {
244
+ try {
245
+ if (existsSync(temporary)) unlinkSync(temporary);
246
+ } finally {
247
+ try {
248
+ closeSync(lockFd);
249
+ } finally {
250
+ unlinkSync(lock);
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ export function selectionChanges(beforeConfig, afterConfig) {
257
+ return [
258
+ { key: "modifiers", before: beforeConfig.modifiers ?? [], after: afterConfig.modifiers ?? [] },
259
+ {
260
+ key: "skills.include",
261
+ before: beforeConfig.skills?.include ?? [],
262
+ after: afterConfig.skills?.include ?? [],
263
+ },
264
+ {
265
+ key: "skills.exclude",
266
+ before: beforeConfig.skills?.exclude ?? [],
267
+ after: afterConfig.skills?.exclude ?? [],
268
+ },
269
+ ].filter(({ before, after }) => JSON.stringify(before) !== JSON.stringify(after));
270
+ }
271
+
272
+ export function configCommand(argv) {
273
+ const command = argv[0] ?? "help";
274
+ if (command === "help" || command === "--help") return console.log(renderHelp());
275
+ if (!COMMANDS.includes(command))
276
+ throw new Error(`unknown config command "${command}" (see config help)`);
277
+ const { values } = parseArgs({
278
+ args: argv.slice(1),
279
+ options: {
280
+ config: { type: "string" },
281
+ help: { type: "boolean" },
282
+ json: { type: "boolean" },
283
+ rules: { type: "boolean" },
284
+ questions: { type: "string" },
285
+ apply: { type: "boolean" },
286
+ expect: { type: "string" },
287
+ ...Object.fromEntries(TOGGLES.map((key) => [key, { type: "string", multiple: true }])),
288
+ },
289
+ });
290
+ if (values.help) return console.log(renderHelp());
291
+ const changing = Boolean(
292
+ values.questions !== undefined || TOGGLES.some((key) => values[key]?.length),
293
+ );
294
+ if (
295
+ (changing || values.apply || values.expect !== undefined) &&
296
+ !["set", "preview"].includes(command)
297
+ )
298
+ throw new Error("selection flags belong to config set or preview");
299
+ if (values.apply && command !== "set") throw new Error("--apply belongs to config set");
300
+ if (values.expect !== undefined && !values.apply) throw new Error("--expect requires --apply");
301
+ if (values.rules && values.json) throw new Error("choose --rules or --json, not both");
302
+ if (values.rules && command !== "preview") throw new Error("--rules belongs to config preview");
303
+ const snapshot = readConfiguration(configurationPath(values.config));
304
+ const next = changing ? changeSelection(snapshot.config, values, snapshot.path) : snapshot.config;
305
+ const report = selectionReport(next, snapshot.path);
306
+ const changes = selectionChanges(snapshot.config, next);
307
+ const applied = values.apply ? writeConfiguration(snapshot, next, values.expect) : null;
308
+ const output = {
309
+ command,
310
+ configPath: snapshot.path,
311
+ exists: values.apply || snapshot.text !== null,
312
+ revision: applied?.revision ?? snapshot.revision,
313
+ changes,
314
+ applied,
315
+ ...report,
316
+ };
317
+ if (values.rules) return process.stdout.write(report.composedRules);
318
+ if (values.json) console.log(JSON.stringify(output, null, 2));
319
+ else console.log(renderConfig(output));
320
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,193 @@
1
+ # Configure house rules
2
+
3
+ Use `node compose.mjs config help` to see the commands. Bun supports the same
4
+ interface. The CLI manages the existing `house-rules.json`; it keeps personal
5
+ layers, independent skill names and unrelated settings.
6
+
7
+ ## Guided selection
8
+
9
+ Run `node compose.mjs setup --config <config-path>` in a terminal. Bun supports
10
+ the same command. The numbered flow uses ordinary line input; `--plain` selects
11
+ the same flow. Both input and output must be terminals. Redirected input or
12
+ output prints scripting instructions and exits 2 without saving. Use
13
+ `setup --json` for the existing read-only `config status --json` report.
14
+
15
+ Modifiers and skills have stable row numbers while searching. Use `toggle N`
16
+ or `toggle NAME`, with several choices separated by spaces. `search TEXT`,
17
+ `clear search`, `show selected` and `show all` change visibility only.
18
+ `help N` shows the full description, source and invocation policy.
19
+ Excluding a skill name excludes its personal override as well as the shipped
20
+ copy. Exclusions without a current source remain in the configuration.
21
+
22
+ Use `questions plain|coded|cards` for question preferences; these modifiers
23
+ are handled together rather than as individual toggles. `next` moves from
24
+ modifiers to skills, then review. `back` keeps the draft. Only `save` on review
25
+ writes configuration. Before a save attempt, `cancel`, end of input or Ctrl-C
26
+ leaves it unchanged.
27
+ A new config with no selection changes is not created.
28
+
29
+ Review includes personal rule origins and an equivalent `config set` command.
30
+ The command uses POSIX quoting on Unix and PowerShell quoting on Windows.
31
+ Personal prose can override the selected preferences; setup does not interpret
32
+ or edit it. Existing composition errors still block saving. Unknown stored
33
+ modifiers can be removed, and a question preset can repair its prerequisite.
34
+ Malformed configuration, legacy skill names and invalid layers require repair
35
+ outside setup.
36
+
37
+ If another writer changes configuration before save, setup preserves the
38
+ draft intent and refuses the stale write. Use `refresh` to compare that intent
39
+ with the fresh configuration, then explicitly save again. It never saves a
40
+ fresh comparison automatically. Saving preserves custom keys and their meaning;
41
+ the writer formats JSON, so existing whitespace is not preserved.
42
+
43
+ If verification fails after saving, configuration may already have changed.
44
+ Inspect `config status` or use `refresh` before another save. Cancelling ends
45
+ the session without undoing an earlier save attempt.
46
+
47
+ Setup saves selections only. Recompose output separately, then connect the
48
+ chosen hosts. It does not install, update, repair or remove managed output.
49
+
50
+ ## Browse and inspect
51
+
52
+ ```bash
53
+ node compose.mjs config catalog --config <config-path>
54
+ node compose.mjs config status --config <config-path>
55
+ node compose.mjs config validate --config <config-path>
56
+ ```
57
+
58
+ `setup` initially shows current selections. Search for relevant additions or
59
+ use `show all` to browse the complete catalog. Review and saving do not require
60
+ a catalog-wide decision.
61
+
62
+ `catalog` shows modifiers, skills and rule sections. `status` shows enabled
63
+ selections and effective personal sources. Both use aligned tables, with color
64
+ in a terminal and stacked rows on narrow screens. `NO_COLOR` disables color.
65
+ Use `--json` for structured output without terminal styling.
66
+
67
+ A missing config is treated as an empty selection without creating files.
68
+ Modifiers are opt-in. Fresh installers copy the sample config with eight skills
69
+ and no modifiers. A missing config or an existing config without `skills.include`
70
+ keeps the full shipped set. The default config path is the same as the composer
71
+ uses. Pass `--config` to choose another.
72
+
73
+ `skills.include` is an optional list of shipped skill names. An empty list selects
74
+ no shipped skills; later shipped additions stay off unless listed. Unknown names,
75
+ malformed lists and duplicates are errors. Personal layer skills still load and
76
+ override shipped skills, even when the shipped name is absent from the list.
77
+ `skills.exclude` takes precedence across every layer. Existing independent names,
78
+ layers and custom settings retain their behavior. The catalog remains complete
79
+ so you can discover additions without enabling them.
80
+
81
+ Status distinguishes configured question preferences from effective rule
82
+ sources. Personal rules can override those preferences. Neither status nor
83
+ validation proves what a running agent has loaded.
84
+
85
+ ## Preview and save
86
+
87
+ ```bash
88
+ node compose.mjs config set --config <config-path> \
89
+ --enable-modifier swarmail --disable-skill hr-read-reddit --questions coded
90
+ ```
91
+
92
+ `set` previews by default. It shows the selection changes, composition summary,
93
+ personal rule overrides and current revision. Repeat the exact command with
94
+ `--apply --expect <revision>` to save the reviewed change. Read the new revision
95
+ from the result before another write.
96
+
97
+ Toggle flags can repeat. Enable or disable modifiers with `--enable-modifier`
98
+ and `--disable-modifier`; skills use `--enable-skill` and `--disable-skill`.
99
+ Enabling a skill removes its exclusion. When `skills.include` exists, enabling
100
+ a shipped skill also adds it to that list. Enabling a personal-only skill does
101
+ not add a shipped name. Disabling adds an exclusion, retaining the include list,
102
+ unrelated exclusions and custom settings. Configs without an include list keep
103
+ their existing selection behavior.
104
+
105
+ | Question preset | Enabled offer modifiers |
106
+ | ------------------- | ----------------------------------- |
107
+ | `--questions plain` | Neither offer modifier |
108
+ | `--questions coded` | `coded-offers` |
109
+ | `--questions cards` | `coded-offers` and `question-cards` |
110
+
111
+ Question presets preserve other modifiers. Choose a preset or explicit offer
112
+ modifier toggles, rather than combining both in one command. Cards still depend
113
+ on the agent host, provider and model. Review any personal Offers override
114
+ before saving; the CLI does not rewrite personal prose.
115
+
116
+ To repair invalid modifier selections, preview changes that produce a valid
117
+ selection. `--questions` can repair the offer dependency; `--disable-modifier`
118
+ can remove an unknown modifier already stored in the config. Malformed config
119
+ structure, unknown enables and unknown disables absent from the config remain
120
+ errors.
121
+
122
+ `preview` accepts the same selection flags without saving. Add `--rules` to
123
+ print the composed rules, or `--json` for the full report. Unknown choices,
124
+ contradictory flags, malformed config and composition errors stop the command
125
+ before it writes configuration. Older skill names need the
126
+ [skill name upgrade](skill-names.md) before composition.
127
+
128
+ ## Write safeguards
129
+
130
+ Saving requires the revision reported by status or preview. A stale revision
131
+ fails without overwriting a newer config. Cooperating CLI writers share an
132
+ exclusive `<config-path>.lock` file; a competing writer fails immediately.
133
+ The CLI writes a sibling temporary file, then replaces the config atomically.
134
+ Repeated requests leave an unchanged config's bytes and modification time alone.
135
+ A missing config is created only by explicit apply.
136
+
137
+ Manual editors do not share the lock. Avoid editing the config while a CLI
138
+ write runs. The CLI checks again before replacement, but cannot exclude a
139
+ manual write racing that final replacement. It refuses symlink config files.
140
+ If an interrupted writer leaves a lock, inspect its recorded process ID and
141
+ confirm that writer has ended before removing the lock. The CLI never removes
142
+ another writer's lock automatically.
143
+
144
+ ## Compose and connect
145
+
146
+ Saving changes only configuration. Recompose with the existing composer or run
147
+ the installer again. The installer retains previous composed skill directories;
148
+ manual `--skills-out` still requires an empty or absent directory.
149
+
150
+ ```bash
151
+ node compose.mjs --config <config-path> --out <rules-path> \
152
+ --skills-out <new-skills-directory>
153
+ ```
154
+
155
+ Connecting or copying that output to agent hosts remains a separate setup
156
+ step. Use `hr-house-rules-setup` for the guided workflow. Custom rule prose
157
+ still belongs in layer fragments; the protected completion sections stay
158
+ required.
159
+
160
+ ## Reference output
161
+
162
+ `--out rules.md` writes selected procedure references to a sibling
163
+ `house-rules-references/` directory. Keep that directory beside the rules file
164
+ when copying it to a host. References do not depend on `skills.include` or
165
+ `skills.exclude`. Core Landing and Reporting always contribute their references; enabled modifiers
166
+ contribute theirs. Reporting is also bundled with `hr-what-next` for standalone
167
+ skill use. Excluding that skill does not remove the core reference. A personal replacement suppresses the resource it replaces.
168
+
169
+ The `multi-agent` modifier exports its complete agent-work procedure when enabled,
170
+ including review follow-ups and cleanup qualification. It remains independent
171
+ of selected skills; enabling reference output does not enable the modifier.
172
+
173
+ Use `--references-out <directory>` with `--out` to choose another destination;
174
+ links are rendered relative to the rules file. Keep both locations available.
175
+ Stdout and ordinary composition/configuration API calls retain full inline
176
+ procedures, so existing preview and copy-only workflows need no bundle.
177
+
178
+ A hash manifest permits repeat exports and updates to previously generated
179
+ files. Modified, missing or unrecognized files in that directory stop generation
180
+ before output writes. Reconcile them explicitly; generation never removes old
181
+ procedure files or unrelated files. Output directories must not overlap.
182
+
183
+ Core rule frontmatter may name one `reference` or a comma-separated `references`
184
+ list relative to `rules/core.md`. Use only one form; list paths and section headings
185
+ must be distinct and non-empty. Each target must be a regular file within the
186
+ source layer, with one level-2 heading matching the core section holding its
187
+ mandatory link. Default composition expands that section;
188
+ linked output retains its safety boundaries and reading trigger. Modifier
189
+ references use the same validation and output inventory.
190
+
191
+ For low-level `compose(coreText, fragments)`, use `loadFragments` for personal
192
+ layers so reference paths retain their source ownership. Directly parsed shipped
193
+ modifiers are recognized only when their label and body match the shipped file.
@@ -0,0 +1,45 @@
1
+ # npm release setup
2
+
3
+ The package name is `@bompus/house-rules`; the executable is `house-rules`.
4
+ It has no runtime dependencies or install hooks. Installing the package does
5
+ not install skills into an agent host. Composition writes the requested rules
6
+ file, its `house-rules-references/` directory when the rules include reference
7
+ procedures, and any `--skills-out` directory.
8
+
9
+ Keep the `package.json` version, changelog version and `v<version>` tag
10
+ identical.
11
+
12
+ ## First publication
13
+
14
+ This one-time setup applies to 0.11.0, the first published version. The first publication requires an npm account that owns the `bompus` scope.
15
+ An earlier publication of another package does not establish current login or
16
+ scope access. Authenticate using npm's web login and complete its 2FA approval.
17
+ Do not put credentials into the repository or workflow.
18
+
19
+ On the selected release commit, run `npm run check:package -- --out <directory>`.
20
+ Publish that verified `house-rules.tgz` with `npm publish <tarball> --access public`.
21
+ Then configure its trusted publisher on npm for GitHub user `bompus`, repository
22
+ `house-rules`, workflow filename `npm-publish.yml`. Enable direct `npm publish`.
23
+ The default staged-publication permission alone does not permit this workflow.
24
+
25
+ The first GitHub release workflow checks the published tarball integrity and
26
+ skips an identical existing version. Different bytes at the same version fail
27
+ the workflow. Subsequent versions publish automatically after trusted publisher
28
+ setup. Do not create another release solely to test authentication.
29
+
30
+ ## Subsequent releases
31
+
32
+ Publishing a stable GitHub release triggers `.github/workflows/npm-publish.yml`.
33
+ It verifies that the tagged commit belongs to `main`, checks the package version
34
+ and dated changelog entry, and checks the packed CLI and skill resources before
35
+ publishing the same tarball. Prereleases are excluded.
36
+
37
+ The workflow uses a GitHub-hosted runner and npm's OIDC trusted publishing.
38
+ It stores no npm token. npm requires Node 22.14 or newer and npm 11.5.1 or newer;
39
+ the workflow uses Node 26. npm automatically supplies provenance for this public
40
+ repository and package. See the [official trusted publishing documentation](https://docs.npmjs.com/trusted-publishers/).
41
+
42
+ CI runs `npm run check:package -- --bun`. It installs the actual local tarball
43
+ into isolated scratch with lifecycle scripts disabled, exercises the CLI under
44
+ Node and Bun, and compares composed skills and resources with the source.
45
+ Scratch honors `HOUSE_RULES_TEST_TMP` and is removed when the check exits normally.
@@ -0,0 +1,50 @@
1
+ # Upgrading skill names
2
+
3
+ Every skill shipped by house-rules uses the `hr-` prefix. For example,
4
+ `handoff` becomes `hr-handoff`. Rule headings, modifier names and the
5
+ `house-rules` command stay the same. Skills from other sources keep their names.
6
+
7
+ ## Before updating
8
+
9
+ 1. In `house-rules.json`, replace each shipped skill name in `skills.exclude`
10
+ with its `hr-` name.
11
+ 2. For each personal override of a shipped skill, rename its directory and
12
+ its frontmatter `name:` to the matching `hr-` name.
13
+ 3. Update your skill invocations, rules, links and host settings to use the
14
+ new names.
15
+
16
+ Composition refuses unresolved old exclusion names, ambiguous old personal
17
+ folders and legacy frontmatter names in prefixed overrides before writing output. The error names the replacement. Fix the
18
+ config or layer, then compose again. The installer keeps the previous composed
19
+ skills until composition succeeds.
20
+
21
+ A personal override named `hr-handoff` replaces the shipped `hr-handoff`.
22
+ If your generic `handoff` is a separate skill you intend to keep alongside it,
23
+ record that decision in your config:
24
+
25
+ ```json
26
+ {
27
+ "skills": {
28
+ "independent": ["handoff"],
29
+ "exclude": ["hr-read-reddit"]
30
+ }
31
+ }
32
+ ```
33
+
34
+ `skills.independent` lets the listed personal skills keep names that match old
35
+ house-rules names. It does not create an alias or replace the prefixed skill.
36
+ `skills.exclude` uses the new names for shipped skills. An old name listed in
37
+ `skills.independent` refers to the independent personal skill and can be excluded
38
+ without excluding its prefixed counterpart. Unrelated personal
39
+ names need no entry in `skills.independent`.
40
+
41
+ ## Connect hosts after composing
42
+
43
+ If a host reads the whole composed directory, point it at the new composed set.
44
+ If it has individual skill links or copied folders, replace its old house-rules
45
+ entries with the matching prefixed entries. Preserve unrelated skills.
46
+ Restart or reload the host and check that it discovers the new names.
47
+
48
+ The composer and installer do not edit host files or remove old host entries.
49
+ Do not keep both the old and new house-rules copies active. Historical skill
50
+ invocations need the new names; there are no compatibility aliases.
@@ -0,0 +1,15 @@
1
+ {
2
+ "modifiers": [],
3
+ "skills": {
4
+ "include": [
5
+ "hr-house-rules-setup",
6
+ "hr-code-review",
7
+ "hr-diagnosing-bugs",
8
+ "hr-explain-code",
9
+ "hr-lean-plan",
10
+ "hr-test-audit",
11
+ "hr-plain-prose",
12
+ "hr-what-next"
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ after: Implementation economy
3
+ ---
4
+
5
+ ## My tooling
6
+
7
+ Use `pnpm` for JavaScript projects unless the repository already uses another
8
+ package manager. Run the project's own lint and test scripts before committing.
@@ -0,0 +1,9 @@
1
+ ---
2
+ replaces: Reporting
3
+ ---
4
+
5
+ ## Reporting
6
+
7
+ Lead with the result in one sentence, then a short list of what changed, what
8
+ was checked and what is left. Keep reports under 15 lines unless asked for
9
+ detail.
@@ -0,0 +1,18 @@
1
+ # Example project
2
+
3
+ ## Session
4
+
5
+ Before your first reply in a session, read § End of every reply and § Offers in
6
+ your user-level house rules in full. They apply here unchanged; this file only
7
+ adds to them and never removes the offer.
8
+
9
+ ## Landing
10
+
11
+ This repository lands by direct push to `main` after `npm test` passes; that
12
+ is the landing path § Landing asks the repository to name. Never force-push
13
+ `main`.
14
+
15
+ ## Commands
16
+
17
+ - Install: `npm ci`
18
+ - Test: `npm test`