@bompus/house-rules 0.0.0-stage → 0.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 (236) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONTRIBUTING.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +378 -2
  5. package/THIRD_PARTY_NOTICES.md +188 -0
  6. package/compose.mjs +125 -0
  7. package/composition.mjs +361 -0
  8. package/config-view.mjs +209 -0
  9. package/config.mjs +381 -0
  10. package/docs/assets/banner-dark.png +0 -0
  11. package/docs/assets/banner-light.png +0 -0
  12. package/docs/assets/overview.png +0 -0
  13. package/docs/configuration.md +269 -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 +42 -4
  20. package/rule-references.mjs +91 -0
  21. package/rules/core.md +340 -0
  22. package/rules/modifiers/backlog-outline.md +25 -0
  23. package/rules/modifiers/backlog-visual.md +16 -0
  24. package/rules/modifiers/coded-offers.md +70 -0
  25. package/rules/modifiers/cross-session-relay.md +23 -0
  26. package/rules/modifiers/effort-estimates.md +41 -0
  27. package/rules/modifiers/land-when-done.md +12 -0
  28. package/rules/modifiers/low-quota-handoff.md +20 -0
  29. package/rules/modifiers/model-default-questions.md +33 -0
  30. package/rules/modifiers/multi-agent.md +38 -0
  31. package/rules/modifiers/no-attribution.md +16 -0
  32. package/rules/modifiers/periodic-updates.md +25 -0
  33. package/rules/modifiers/plan-files.md +82 -0
  34. package/rules/modifiers/question-cards.md +48 -0
  35. package/rules/modifiers/release-batching.md +15 -0
  36. package/rules/modifiers/scratch-on-disk.md +15 -0
  37. package/rules/modifiers/shared-host-load.md +19 -0
  38. package/rules/modifiers/solo-operator.md +13 -0
  39. package/rules/modifiers/squash-landing.md +22 -0
  40. package/rules/modifiers/upstream-contributions.md +45 -0
  41. package/rules/modifiers/web-image-formats.md +35 -0
  42. package/rules/references/backlog-view.md +154 -0
  43. package/rules/references/github-actions.md +45 -0
  44. package/rules/references/landing.md +39 -0
  45. package/rules/references/multi-agent.md +51 -0
  46. package/rules/references/release-batching.md +66 -0
  47. package/rules/references/squash-landing.md +50 -0
  48. package/rules/references/thread-visuals.md +45 -0
  49. package/setup.mjs +445 -0
  50. package/skills/hr-agent-guidance-audit/LICENSE +21 -0
  51. package/skills/hr-agent-guidance-audit/SKILL.md +91 -0
  52. package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
  53. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  54. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
  55. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  56. package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
  57. package/skills/hr-agent-guidance-refresh/SKILL.md +36 -0
  58. package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
  59. package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
  60. package/skills/hr-api-exposure-check/SKILL.md +159 -0
  61. package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
  62. package/skills/hr-audit-choices/SKILL.md +67 -0
  63. package/skills/hr-audit-choices/agents/openai.yaml +2 -0
  64. package/skills/hr-benchmarking/NOTICE.md +36 -0
  65. package/skills/hr-benchmarking/SKILL.md +279 -0
  66. package/skills/hr-benchmarking/agents/openai.yaml +2 -0
  67. package/skills/hr-benchmarking/references/javascript.md +130 -0
  68. package/skills/hr-benchmarking/references/tools.md +165 -0
  69. package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
  70. package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
  71. package/skills/hr-better-accessibility/LICENSE +22 -0
  72. package/skills/hr-better-accessibility/NOTICE.md +46 -0
  73. package/skills/hr-better-accessibility/SKILL.md +119 -0
  74. package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
  75. package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
  76. package/skills/hr-better-accessibility/forms.md +82 -0
  77. package/skills/hr-better-accessibility/hit-areas.md +98 -0
  78. package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
  79. package/skills/hr-better-accessibility/screen-readers.md +102 -0
  80. package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
  81. package/skills/hr-better-colors/LICENSE +21 -0
  82. package/skills/hr-better-colors/NOTICE.md +37 -0
  83. package/skills/hr-better-colors/SKILL.md +114 -0
  84. package/skills/hr-better-colors/agents/openai.yaml +3 -0
  85. package/skills/hr-better-colors/color-formats.md +90 -0
  86. package/skills/hr-better-colors/color-usage.md +140 -0
  87. package/skills/hr-better-colors/contrast.md +80 -0
  88. package/skills/hr-better-colors/palette-generation.md +104 -0
  89. package/skills/hr-better-colors/palette-structure.md +76 -0
  90. package/skills/hr-better-colors/token-naming.md +97 -0
  91. package/skills/hr-better-layout/LICENSE +22 -0
  92. package/skills/hr-better-layout/NOTICE.md +48 -0
  93. package/skills/hr-better-layout/SKILL.md +92 -0
  94. package/skills/hr-better-layout/agents/openai.yaml +3 -0
  95. package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
  96. package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
  97. package/skills/hr-better-typography/LICENSE +22 -0
  98. package/skills/hr-better-typography/NOTICE.md +46 -0
  99. package/skills/hr-better-typography/SKILL.md +172 -0
  100. package/skills/hr-better-typography/agents/openai.yaml +3 -0
  101. package/skills/hr-better-typography/choosing-fonts.md +69 -0
  102. package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
  103. package/skills/hr-better-typography/details-and-accessibility.md +119 -0
  104. package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
  105. package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
  106. package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
  107. package/skills/hr-better-writing/LICENSE +22 -0
  108. package/skills/hr-better-writing/NOTICE.md +46 -0
  109. package/skills/hr-better-writing/SKILL.md +122 -0
  110. package/skills/hr-better-writing/agents/openai.yaml +3 -0
  111. package/skills/hr-code-review/LICENSE +21 -0
  112. package/skills/hr-code-review/NOTICE.md +26 -0
  113. package/skills/hr-code-review/SKILL.md +163 -0
  114. package/skills/hr-code-review/agents/openai.yaml +3 -0
  115. package/skills/hr-code-review/references/acp-client.md +43 -0
  116. package/skills/hr-code-review/references/evidence.md +57 -0
  117. package/skills/hr-code-review/references/impact.md +103 -0
  118. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  119. package/skills/hr-code-review/references/panel-plan.md +61 -0
  120. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  121. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  122. package/skills/hr-code-review/references/panel-setup.md +54 -0
  123. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  124. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  125. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  126. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  127. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  128. package/skills/hr-design-exploration/SKILL.md +105 -0
  129. package/skills/hr-design-exploration/agents/openai.yaml +4 -0
  130. package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
  131. package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
  132. package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
  133. package/skills/hr-diagnosing-bugs/references/reporting.md +97 -0
  134. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  135. package/skills/hr-explain-code/NOTICE.md +37 -0
  136. package/skills/hr-explain-code/SKILL.md +110 -0
  137. package/skills/hr-explain-code/agents/openai.yaml +5 -0
  138. package/skills/hr-explain-code/references/helper-brief.md +39 -0
  139. package/skills/hr-extract-shared-steps/SKILL.md +134 -0
  140. package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
  141. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  142. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  143. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  144. package/skills/hr-handoff/LICENSE +21 -0
  145. package/skills/hr-handoff/NOTICE.md +29 -0
  146. package/skills/hr-handoff/SKILL.md +114 -0
  147. package/skills/hr-handoff/agents/openai.yaml +5 -0
  148. package/skills/hr-handoff/assets/ledger.example.json +10 -0
  149. package/skills/hr-handoff/assets/receipt.example.json +76 -0
  150. package/skills/hr-handoff/assets/source.example.json +20 -0
  151. package/skills/hr-handoff/references/reconciliation.md +124 -0
  152. package/skills/hr-handoff/references/successor-thread.md +76 -0
  153. package/skills/hr-handoff/scripts/check-reconciliation.mjs +324 -0
  154. package/skills/hr-house-rules-setup/SKILL.md +114 -0
  155. package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
  156. package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
  157. package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
  158. package/skills/hr-lean-plan/SKILL.md +146 -0
  159. package/skills/hr-lean-plan/agents/openai.yaml +5 -0
  160. package/skills/hr-maintainability-review/SKILL.md +186 -0
  161. package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
  162. package/skills/hr-manual-qa/NOTICE.md +36 -0
  163. package/skills/hr-manual-qa/SKILL.md +37 -0
  164. package/skills/hr-manual-qa/agents/openai.yaml +3 -0
  165. package/skills/hr-navigation-retro/LICENSE +21 -0
  166. package/skills/hr-navigation-retro/NOTICE.md +5 -0
  167. package/skills/hr-navigation-retro/SKILL.md +86 -0
  168. package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
  169. package/skills/hr-navigation-retro/references/inputs.md +67 -0
  170. package/skills/hr-navigation-retro/scripts/retro.py +552 -0
  171. package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
  172. package/skills/hr-ordering-tests/SKILL.md +51 -0
  173. package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
  174. package/skills/hr-plain-prose/SKILL.md +264 -0
  175. package/skills/hr-plain-prose/agents/openai.yaml +3 -0
  176. package/skills/hr-plain-prose/scripts/check.mjs +331 -0
  177. package/skills/hr-pr-followup/LICENSE +21 -0
  178. package/skills/hr-pr-followup/NOTICE.md +9 -0
  179. package/skills/hr-pr-followup/SKILL.md +74 -0
  180. package/skills/hr-pr-followup/agents/openai.yaml +2 -0
  181. package/skills/hr-pr-followup/references/review-helpers.md +111 -0
  182. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  183. package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
  184. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  185. package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
  186. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  187. package/skills/hr-progress-report/SKILL.md +49 -0
  188. package/skills/hr-progress-report/agents/openai.yaml +5 -0
  189. package/skills/hr-read-reddit/SKILL.md +44 -0
  190. package/skills/hr-read-reddit/agents/openai.yaml +2 -0
  191. package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
  192. package/skills/hr-read-x-links/SKILL.md +42 -0
  193. package/skills/hr-read-x-links/agents/openai.yaml +2 -0
  194. package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
  195. package/skills/hr-research-shared-links/SKILL.md +103 -0
  196. package/skills/hr-split-to-prs/LICENSE +21 -0
  197. package/skills/hr-split-to-prs/NOTICE.md +9 -0
  198. package/skills/hr-split-to-prs/SKILL.md +61 -0
  199. package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
  200. package/skills/hr-sqlite/SKILL.md +174 -0
  201. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  202. package/skills/hr-sqlite/references/evidence.md +94 -0
  203. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  204. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  205. package/skills/hr-stock-ui-audit/SKILL.md +132 -0
  206. package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
  207. package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
  208. package/skills/hr-tdd/LICENSE +21 -0
  209. package/skills/hr-tdd/NOTICE.md +10 -0
  210. package/skills/hr-tdd/SKILL.md +40 -0
  211. package/skills/hr-tdd/agents/openai.yaml +3 -0
  212. package/skills/hr-tdd/mocking.md +39 -0
  213. package/skills/hr-tdd/tests.md +77 -0
  214. package/skills/hr-test-audit/LICENSE +21 -0
  215. package/skills/hr-test-audit/NOTICE.md +67 -0
  216. package/skills/hr-test-audit/SKILL.md +146 -0
  217. package/skills/hr-test-audit/agents/openai.yaml +3 -0
  218. package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
  219. package/skills/hr-usage-report/SKILL.md +90 -0
  220. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  221. package/skills/hr-usage-report/assets/example-report.json +125 -0
  222. package/skills/hr-usage-report/assets/report-template.html +377 -0
  223. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  224. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  225. package/skills/hr-what-next/SKILL.md +151 -0
  226. package/skills/hr-what-next/agents/openai.yaml +4 -0
  227. package/skills/hr-what-next/references/reporting.md +59 -0
  228. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  229. package/skills/hr-writing-for-agents/NOTICE.md +46 -0
  230. package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
  231. package/skills/hr-writing-for-agents/SKILL.md +108 -0
  232. package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
  233. package/skills/hr-writing-pr/NOTICE.md +48 -0
  234. package/skills/hr-writing-pr/SKILL.md +119 -0
  235. package/skills/hr-writing-pr/agents/openai.yaml +2 -0
  236. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
@@ -0,0 +1,299 @@
1
+ // ACP v1 read-only review transport. See ../references/acp-client.md.
2
+ import { execFileSync, spawn } from "node:child_process";
3
+ import { realpathSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import { isAbsolute, relative, resolve, sep } from "node:path";
6
+ import * as readline from "node:readline";
7
+ const rawArgs = process.argv.slice(2);
8
+ function takeFlag(name, def) {
9
+ const i = rawArgs.indexOf(name);
10
+ if (i >= 0 && i + 1 < rawArgs.length) {
11
+ const v = rawArgs[i + 1];
12
+ rawArgs.splice(i, 2);
13
+ return v;
14
+ }
15
+ return def;
16
+ }
17
+ const maxTurns = Math.max(1, parseInt(takeFlag("--max-turns", "1"), 10));
18
+ const verdictMarker = takeFlag("--verdict", "");
19
+ const modelArg = takeFlag("--model", "");
20
+ const effortArg = takeFlag("--effort", "");
21
+ const timeoutMin = Math.max(1, Number(takeFlag("--timeout-min", "60")) || 60);
22
+ const idleArg = Number(takeFlag("--idle-min", "5"));
23
+ const IDLE_MS = (idleArg > 0 ? idleArg : 5) * 60 * 1000;
24
+ const [serverCmdJson, promptFile, outText, cwdArg] = rawArgs;
25
+ if (!serverCmdJson || !promptFile || !outText) {
26
+ console.error(
27
+ "usage: node acp-panel-client.mjs <server-cmd-json> <prompt-file> <out-text> [cwd] [--max-turns N] [--verdict MARK] [--model ID] [--effort LEVEL] [--idle-min N] [--timeout-min N]",
28
+ );
29
+ process.exit(2);
30
+ }
31
+ const serverCmd = JSON.parse(serverCmdJson);
32
+ const cwd = realpathSync(cwdArg || process.cwd());
33
+ const promptText = readFileSync(promptFile, "utf8");
34
+ const HARD_TIMEOUT_MS = timeoutMin * 60 * 1000;
35
+ function gitStatus() {
36
+ try {
37
+ return execFileSync("git", ["-C", cwd, "status", "--porcelain"], { encoding: "utf8" });
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ const statusBefore = gitStatus();
43
+ const child = spawn(serverCmd[0], serverCmd.slice(1), {
44
+ cwd,
45
+ env: process.env,
46
+ stdio: ["pipe", "pipe", "inherit"],
47
+ });
48
+ let nextId = 1;
49
+ let isCodex = false;
50
+ const pending = new Map();
51
+ function rejectPending(error) {
52
+ for (const p of pending.values()) p.reject(error);
53
+ pending.clear();
54
+ }
55
+ child.on("error", rejectPending);
56
+ child.on("exit", (code, signal) =>
57
+ rejectPending(new Error(`ACP server exited (${code ?? signal})`)),
58
+ );
59
+ const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
60
+ function send(msg) {
61
+ child.stdin.write(JSON.stringify(msg) + "\n");
62
+ }
63
+ function request(method, params) {
64
+ const id = nextId++;
65
+ return new Promise((resolve, reject) => {
66
+ pending.set(id, { resolve, reject });
67
+ send({ jsonrpc: "2.0", id, method, params });
68
+ });
69
+ }
70
+ function replyOk(id, result) {
71
+ send({ jsonrpc: "2.0", id, result });
72
+ }
73
+ function replyErr(id, code, message) {
74
+ send({ jsonrpc: "2.0", id, error: { code, message } });
75
+ }
76
+ // Resolve physical targets; the client is not an OS sandbox for agent-owned tools.
77
+ async function underCwd(path) {
78
+ if (typeof path !== "string" || !path) return null;
79
+ const lexical = resolve(cwd, path);
80
+ const inside = (target) => {
81
+ const rel = relative(cwd, target);
82
+ return rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
83
+ };
84
+ if (!inside(lexical)) return null;
85
+ // Same resolver as the root, so a runtime whose async and sync realpath spell a path
86
+ // differently cannot make an inside file look outside.
87
+ const physical = realpathSync(lexical);
88
+ if (!inside(physical) || !(await stat(physical)).isFile()) return null;
89
+ return physical;
90
+ }
91
+ async function handleServerRequest(msg) {
92
+ const { id, method } = msg;
93
+ const params = msg.params ?? {};
94
+ try {
95
+ if (method === "fs/read_text_file") {
96
+ const ok = await underCwd(params.path);
97
+ if (!ok) {
98
+ return replyErr(id, -32602, "path outside cwd");
99
+ }
100
+ const content = await readFile(ok, "utf8");
101
+ return replyOk(id, { content });
102
+ }
103
+ if (method === "fs/write_text_file") {
104
+ return replyErr(id, -32601, "write_text_file disabled by panel policy");
105
+ }
106
+ if (method?.startsWith("terminal/")) {
107
+ return replyErr(id, -32601, "terminal execution disabled by review policy");
108
+ }
109
+ if (method === "session/request_permission") {
110
+ const opts = params.options || [];
111
+ const kind = params.toolCall?.kind;
112
+ const reject = opts.find((o) => String(o.kind || "").startsWith("reject"));
113
+ console.error(`[client] rejected ${kind || "unknown"} permission request`);
114
+ rejections++;
115
+ return replyOk(
116
+ id,
117
+ reject
118
+ ? { outcome: { outcome: "selected", optionId: reject.optionId } }
119
+ : { outcome: { outcome: "cancelled" } },
120
+ );
121
+ }
122
+ return replyErr(id, -32601, "method not implemented by panel client: " + method);
123
+ } catch (e) {
124
+ return replyErr(id, -32603, e instanceof Error ? e.message : String(e));
125
+ }
126
+ }
127
+ let collected = "";
128
+ let rejections = 0;
129
+ let updateCount = 0;
130
+ rl.on("line", (line) => {
131
+ if (!line.trim()) {
132
+ return;
133
+ }
134
+ let msg;
135
+ try {
136
+ msg = JSON.parse(line);
137
+ } catch {
138
+ return;
139
+ }
140
+ noteActivity();
141
+ if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
142
+ const p = pending.get(msg.id);
143
+ if (p) {
144
+ pending.delete(msg.id);
145
+ if (msg.error) p.reject(new Error(JSON.stringify(msg.error)));
146
+ else p.resolve(msg.result);
147
+ }
148
+ return;
149
+ }
150
+ if (msg.id !== undefined && msg.method) {
151
+ handleServerRequest(msg);
152
+ return;
153
+ }
154
+ if (msg.method === "session/update") {
155
+ const u = msg.params?.update;
156
+ if (
157
+ u?.sessionUpdate === "agent_message_chunk" &&
158
+ u.content &&
159
+ typeof u.content.text === "string"
160
+ ) {
161
+ collected += u.content.text;
162
+ }
163
+ updateCount++;
164
+ if (updateCount % 25 === 0) {
165
+ console.error(`[client] ${updateCount} session updates...`);
166
+ }
167
+ return;
168
+ }
169
+ });
170
+ let lastActivity = Date.now();
171
+ let longestGapMs = 0;
172
+ function noteActivity() {
173
+ const now = Date.now();
174
+ longestGapMs = Math.max(longestGapMs, now - lastActivity);
175
+ lastActivity = now;
176
+ }
177
+ const gapNote = () => `longest silent gap ${Math.round(longestGapMs / 1000)}s`;
178
+ let deadlineHit = false;
179
+ function abort(reason) {
180
+ deadlineHit = true;
181
+ console.error(`[client] ${reason} (${gapNote()}), killing server`);
182
+ try {
183
+ if (sessionId) send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
184
+ } catch {}
185
+ setTimeout(() => {
186
+ child.kill("SIGKILL");
187
+ process.exit(124);
188
+ }, 2000).unref();
189
+ }
190
+ const timer = setTimeout(() => abort("HARD TIMEOUT"), HARD_TIMEOUT_MS).unref();
191
+ const idleCheck = setInterval(
192
+ () => {
193
+ if (Date.now() - lastActivity > IDLE_MS) {
194
+ clearInterval(idleCheck);
195
+ abort(`IDLE TIMEOUT: no server message for ${Math.round(IDLE_MS / 1000)}s`);
196
+ }
197
+ },
198
+ Math.min(IDLE_MS / 4, 15000),
199
+ ).unref();
200
+ let sessionId;
201
+ try {
202
+ const init = await request("initialize", {
203
+ protocolVersion: 1,
204
+ clientCapabilities: { fs: { readTextFile: true, writeTextFile: false }, terminal: false },
205
+ clientInfo: { name: "acp-panel-client", version: "0.2.0" },
206
+ });
207
+ if (init.protocolVersion !== 1) throw new Error("ACP protocol version 1 required");
208
+ const agentName = (init.agentInfo && (init.agentInfo.name || init.agentInfo.title)) || "unknown";
209
+ isCodex = /codex/i.test(agentName);
210
+ console.error(`[client] initialized, protocol=${init.protocolVersion}, agent=${agentName}`);
211
+ send({ jsonrpc: "2.0", method: "initialized", params: {} });
212
+ const s = await request("session/new", { cwd, mcpServers: [] });
213
+ sessionId = s.sessionId;
214
+ if (typeof sessionId !== "string" || !sessionId) throw new Error("Missing session identity");
215
+ let options = s.configOptions || [];
216
+ const modeRequired = isCodex || options.some((o) => o.id === "mode");
217
+ const required = new Map();
218
+ // Set responses are complete snapshots; later changes must preserve earlier bindings.
219
+ async function bind(ids, value, optional = false) {
220
+ const matches = options.filter((o) => ids.includes(o.id));
221
+ if (matches.length !== 1) {
222
+ if (optional && !matches.length) return;
223
+ throw new Error(`agent exposes no unique ${ids.join("/")} option; cannot bind ${value}`);
224
+ }
225
+ const option = matches[0];
226
+ const values = (option.options || []).flatMap((o) => o.options || [o]);
227
+ if (!values.some((o) => o.value === value))
228
+ throw new Error(`Unsupported ${option.id} value: ${value}`);
229
+ required.set(option.id, value);
230
+ const res = await request("session/set_config_option", {
231
+ sessionId,
232
+ configId: option.id,
233
+ value,
234
+ });
235
+ if (!Array.isArray(res?.configOptions)) throw new Error(`Missing ${option.id} confirmation`);
236
+ options = res.configOptions;
237
+ for (const [id, expected] of required) {
238
+ const echoed = options.filter((o) => o.id === id);
239
+ if (echoed.length !== 1 || echoed[0].currentValue !== expected)
240
+ throw new Error(`Unverified ${id} binding`);
241
+ }
242
+ console.error(`[client] ${option.id} -> ${value}`);
243
+ }
244
+ if (modelArg) await bind(["model"], modelArg);
245
+ if (effortArg) await bind(["effort", "reasoning_effort"], effortArg);
246
+ await bind(["mode"], "read-only", !modeRequired);
247
+ console.error(
248
+ `[client] session ${sessionId}, maxTurns=${maxTurns}${verdictMarker ? `, verdictMarker=${JSON.stringify(verdictMarker)}` : ""}`,
249
+ );
250
+ let stopReason;
251
+ for (let turn = 1; turn <= maxTurns; turn++) {
252
+ const text =
253
+ turn === 1
254
+ ? promptText
255
+ : `Continue the panel task. Your previous reply did not contain ${JSON.stringify(verdictMarker)}. Do no new setup; use only read/glob/grep on the repo, no MCP tools, and now deliver the full answer ending with ${verdictMarker} plus a value. Keep it under 30 lines.`;
256
+ console.error(`[client] turn ${turn}/${maxTurns}, sending prompt (${text.length} chars)...`);
257
+ const rejectionsBefore = rejections;
258
+ const res = await request("session/prompt", {
259
+ sessionId,
260
+ prompt: [{ type: "text", text }],
261
+ });
262
+ stopReason = res?.stopReason;
263
+ if (deadlineHit) break;
264
+ const hit = verdictMarker ? collected.includes(verdictMarker) : true;
265
+ console.error(
266
+ `[client] turn ${turn} finished, stopReason=${JSON.stringify(stopReason)} collected=${collected.length} chars hasVerdict=${hit}`,
267
+ );
268
+ if (hit) {
269
+ break;
270
+ }
271
+ const cancelledByRejection = stopReason === "cancelled" && rejections > rejectionsBefore;
272
+ if (stopReason && stopReason !== "end_turn" && !cancelledByRejection) {
273
+ break;
274
+ }
275
+ }
276
+ clearTimeout(timer);
277
+ clearInterval(idleCheck);
278
+ child.stdin.end();
279
+ setTimeout(() => child.kill(), 1000).unref();
280
+ const ok = verdictMarker ? collected.includes(verdictMarker) : true;
281
+ const changed = statusBefore !== null && gitStatus() !== statusBefore;
282
+ if (changed) {
283
+ console.error(
284
+ `[client] WORKSPACE CHANGED: git status in ${cwd} differs from launch; the seat wrote files`,
285
+ );
286
+ }
287
+ // The answer is written after the status comparison so an output path inside the review
288
+ // root is not mistaken for a change the seat made.
289
+ writeFileSync(outText, collected);
290
+ console.error(`[client] wrote ${collected.length} chars to ${outText}; ${gapNote()}`);
291
+ process.exitCode = deadlineHit ? 124 : changed ? 4 : ok ? 0 : 3;
292
+ setTimeout(() => process.exit(), 2500).unref();
293
+ } catch (e) {
294
+ console.error("[client] FATAL: " + (e instanceof Error ? e.message : String(e)));
295
+ clearTimeout(timer);
296
+ clearInterval(idleCheck);
297
+ child.kill("SIGKILL");
298
+ process.exit(deadlineHit ? 124 : 1);
299
+ }
@@ -0,0 +1,263 @@
1
+ // Checks the file:line citations in a panel's output against the repository.
2
+ // See ../references/panel-adjudication.md.
3
+ import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
4
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ const EXT =
8
+ "(?:tsx?|jsx?|mjs|cjs|vue|svelte|css|scss|html|md|json|py|sh|yaml|yml|toml|go|rs|rb|java|kt|swift|php|cs|cpp|c|h)";
9
+ const STRICT_RE = new RegExp(
10
+ "`([^`\\n]{1,80})`\\s*\\(\\s*([\\w./\\\\-]+\\." +
11
+ EXT +
12
+ ")\\s*:\\s*(\\d+)(?:\\s*-\\s*(\\d+))?\\s*\\)",
13
+ "g",
14
+ );
15
+ const BARE_RE = new RegExp("([\\w./\\\\-]+\\." + EXT + "):(\\d+)(?:-(\\d+))?", "g");
16
+
17
+ // Panel logs are either plain text or JSON lines whose text parts hold the reply.
18
+ export function extractPanelText(raw) {
19
+ const texts = [];
20
+ for (const line of raw.split("\n")) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed.startsWith("{")) {
23
+ continue;
24
+ }
25
+ try {
26
+ walk(JSON.parse(trimmed), texts);
27
+ } catch {
28
+ // Not JSON; ignore the line.
29
+ }
30
+ }
31
+ // A plain reply may contain a JSON example line; with no text parts, keep the raw text.
32
+ return texts.length === 0 ? raw : texts.join("");
33
+ }
34
+
35
+ function walk(node, out) {
36
+ if (Array.isArray(node)) {
37
+ for (const item of node) {
38
+ walk(item, out);
39
+ }
40
+ return;
41
+ }
42
+ if (node && typeof node === "object") {
43
+ const { type, text } = node;
44
+ if ((type === "text" || type === "text-delta") && typeof text === "string") {
45
+ out.push(text);
46
+ }
47
+ for (const value of Object.values(node)) {
48
+ walk(value, out);
49
+ }
50
+ }
51
+ }
52
+
53
+ export function findCitations(text) {
54
+ const found = [];
55
+ const strictSpans = [];
56
+ for (const match of text.matchAll(STRICT_RE)) {
57
+ strictSpans.push([match.index, match.index + match[0].length]);
58
+ found.push({
59
+ ident: match[1],
60
+ file: match[2],
61
+ start: Number(match[3]),
62
+ end: Number(match[4] ?? match[3]),
63
+ strict: true,
64
+ });
65
+ }
66
+ for (const match of text.matchAll(BARE_RE)) {
67
+ const at = match.index;
68
+ if (strictSpans.some(([from, to]) => at >= from && at < to)) {
69
+ continue;
70
+ }
71
+ found.push({
72
+ ident: null,
73
+ file: match[1],
74
+ start: Number(match[2]),
75
+ end: Number(match[3] ?? match[2]),
76
+ strict: false,
77
+ });
78
+ }
79
+ return found;
80
+ }
81
+
82
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage"]);
83
+
84
+ function isFile(path) {
85
+ try {
86
+ return statSync(path).isFile();
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ const indexCache = new Map();
93
+
94
+ // Models cite basenames (`util.ts:97`) or subpaths (`src/util.ts:97`). Resolve those against
95
+ // the repository tree: one match wins, none fails as unreadable, several fail as ambiguous.
96
+ function resolveByBasename(cited, repoRoot) {
97
+ if (!indexCache.has(repoRoot)) {
98
+ const index = [];
99
+ const walkDir = (dir) => {
100
+ let entries;
101
+ try {
102
+ entries = readdirSync(dir);
103
+ } catch {
104
+ return;
105
+ }
106
+ for (const entry of entries) {
107
+ if (SKIP_DIRS.has(entry)) {
108
+ continue;
109
+ }
110
+ const full = join(dir, entry);
111
+ let stat;
112
+ try {
113
+ stat = statSync(full);
114
+ } catch {
115
+ continue;
116
+ }
117
+ if (stat.isDirectory()) {
118
+ walkDir(full);
119
+ } else if (stat.isFile()) {
120
+ index.push(full);
121
+ }
122
+ }
123
+ };
124
+ walkDir(repoRoot);
125
+ indexCache.set(repoRoot, index);
126
+ }
127
+ const normalized = cited.split(/[\\/]/).join("/");
128
+ const suffix = `/${normalized}`;
129
+ const matches = indexCache
130
+ .get(repoRoot)
131
+ .filter((path) => basename(path) === normalized || path.split(sep).join("/").endsWith(suffix));
132
+ if (matches.length === 1) {
133
+ return matches[0];
134
+ }
135
+ return matches.length > 1 ? matches : null;
136
+ }
137
+
138
+ export function checkCitation(citation, repoRoot) {
139
+ const direct = isAbsolute(citation.file) ? citation.file : resolve(repoRoot, citation.file);
140
+ const path = isFile(direct) ? direct : resolveByBasename(citation.file, repoRoot);
141
+ if (path === null) {
142
+ return { ...citation, path: direct, status: "fail", reason: "unreadable: ENOENT" };
143
+ }
144
+ // Compare real paths so a symlink inside the repository cannot lead outside it.
145
+ let checkedPath = path;
146
+ if (typeof path === "string") {
147
+ try {
148
+ checkedPath = realpathSync(path);
149
+ const away = relative(realpathSync(repoRoot), checkedPath);
150
+ if (away === ".." || away.startsWith(`..${sep}`) || isAbsolute(away)) {
151
+ return { ...citation, path, status: "fail", reason: "outside the repository" };
152
+ }
153
+ } catch (error) {
154
+ return {
155
+ ...citation,
156
+ path,
157
+ status: "fail",
158
+ reason: `unreadable: ${error.code ?? String(error)}`,
159
+ };
160
+ }
161
+ }
162
+ if (Array.isArray(path)) {
163
+ return {
164
+ ...citation,
165
+ path: direct,
166
+ status: "fail",
167
+ reason: `ambiguous: ${path.length} matches`,
168
+ };
169
+ }
170
+ let lines;
171
+ try {
172
+ lines = readFileSync(checkedPath, "utf8").split("\n");
173
+ // A final newline ends the last line; it does not start another.
174
+ if (lines.length > 1 && lines.at(-1) === "") {
175
+ lines.pop();
176
+ }
177
+ } catch (error) {
178
+ return {
179
+ ...citation,
180
+ path,
181
+ status: "fail",
182
+ reason: `unreadable: ${error.code ?? String(error)}`,
183
+ };
184
+ }
185
+ if (!(citation.start >= 1) || citation.end < citation.start) {
186
+ return { ...citation, path, status: "fail", reason: "bad line range" };
187
+ }
188
+ if (citation.end > lines.length) {
189
+ return {
190
+ ...citation,
191
+ path,
192
+ status: "fail",
193
+ reason: `line ${citation.end} past EOF (${lines.length} lines)`,
194
+ };
195
+ }
196
+ if (citation.ident === null) {
197
+ return { ...citation, path, status: "pass", reason: "exists" };
198
+ }
199
+ const window = lines.slice(citation.start - 1, citation.end).join("\n");
200
+ if (!window.includes(citation.ident)) {
201
+ const range =
202
+ citation.end === citation.start ? `${citation.start}` : `${citation.start}-${citation.end}`;
203
+ return {
204
+ ...citation,
205
+ path,
206
+ status: "fail",
207
+ reason: `line ${range} lacks '${citation.ident}'`,
208
+ };
209
+ }
210
+ return { ...citation, path, status: "pass", reason: "cited" };
211
+ }
212
+
213
+ export function checkLogs(files, repoRoot) {
214
+ const results = [];
215
+ for (const file of files) {
216
+ const text = extractPanelText(readFileSync(file, "utf8"));
217
+ for (const citation of findCitations(text)) {
218
+ results.push({ log: file, ...checkCitation(citation, repoRoot) });
219
+ }
220
+ }
221
+ return results;
222
+ }
223
+
224
+ function main(argv) {
225
+ const json = argv.includes("--json");
226
+ const repoFlag = argv.indexOf("--repo");
227
+ const repoRoot = repoFlag >= 0 ? resolve(argv[repoFlag + 1] ?? ".") : process.cwd();
228
+ const files = argv.filter(
229
+ (arg, i) => arg !== "--json" && arg !== "--repo" && argv[i - 1] !== "--repo",
230
+ );
231
+ if (files.length === 0) {
232
+ console.error("Usage: check-receipts.mjs [--repo <dir>] [--json] <panel-log> [more...]");
233
+ return 2;
234
+ }
235
+ let results;
236
+ try {
237
+ results = checkLogs(files, repoRoot);
238
+ } catch (error) {
239
+ console.error(`Cannot read a panel log: ${error.message}`);
240
+ return 2;
241
+ }
242
+ if (results.length === 0) {
243
+ console.error("No citations found.");
244
+ return 2;
245
+ }
246
+ if (json) {
247
+ console.log(JSON.stringify(results, null, 2));
248
+ } else {
249
+ for (const row of results) {
250
+ const label = row.strict
251
+ ? `\`${row.ident}\` (${row.file}:${row.start})`
252
+ : `${row.file}:${row.start}`;
253
+ console.log(`${row.status === "pass" ? "PASS" : "FAIL"} ${label} - ${row.reason}`);
254
+ }
255
+ const failed = results.filter((row) => row.status === "fail").length;
256
+ console.log(`${results.length - failed}/${results.length} citations verified.`);
257
+ }
258
+ return results.some((row) => row.status === "fail") ? 1 : 0;
259
+ }
260
+
261
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
262
+ process.exitCode = main(process.argv.slice(2));
263
+ }
@@ -0,0 +1,69 @@
1
+ // Prints the panel a set of preferences resolves to, before any model is called.
2
+ // See ../references/panel-plan.md.
3
+ import { createHash } from "node:crypto";
4
+ import { lstatSync, readFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join, resolve } from "node:path";
7
+ import { parseArgs } from "node:util";
8
+ import { formatPanelPlan, resolvePanel } from "./panel-preferences.mjs";
9
+
10
+ function readJson(path, label) {
11
+ const file = resolve(path);
12
+ if (!lstatSync(file).isFile())
13
+ throw new Error(`${label} must be a regular file, not a symlink or directory`);
14
+ const text = readFileSync(file, "utf8");
15
+ try {
16
+ return { value: JSON.parse(text), revision: createHash("sha256").update(text).digest("hex") };
17
+ } catch {
18
+ throw new Error(`invalid JSON in ${label}`);
19
+ }
20
+ }
21
+
22
+ function main(argv) {
23
+ const { values } = parseArgs({
24
+ args: argv,
25
+ options: {
26
+ config: { type: "string" },
27
+ bindings: { type: "string" },
28
+ project: { type: "string" },
29
+ "approve-project": { type: "string" },
30
+ task: { type: "string" },
31
+ "author-family": { type: "string", multiple: true },
32
+ "approve-metered": { type: "string", multiple: true },
33
+ json: { type: "boolean" },
34
+ },
35
+ });
36
+ if (values.bindings === undefined) throw new Error("--bindings <json> is required");
37
+ const config = readJson(
38
+ values.config ??
39
+ join(
40
+ process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"),
41
+ "house-rules/house-rules.json",
42
+ ),
43
+ "configuration",
44
+ );
45
+ if (config.value.panel === undefined) throw new Error("configuration has no panel section");
46
+ const project =
47
+ values.project === undefined ? undefined : readJson(values.project, "project preferences");
48
+ const request = {
49
+ user: config.value.panel,
50
+ project: project?.value,
51
+ task: values.task === undefined ? undefined : readJson(values.task, "task preferences").value,
52
+ bindings: readJson(values.bindings, "bindings").value,
53
+ authorFamilies: values["author-family"] ?? [],
54
+ approvals: {
55
+ project: project !== undefined && values["approve-project"] === project.revision,
56
+ metered: values["approve-metered"] ?? [],
57
+ },
58
+ };
59
+ const plan = resolvePanel(request);
60
+ console.log(values.json ? JSON.stringify(plan, null, 2) : formatPanelPlan(plan));
61
+ return plan.ok ? 0 : 1;
62
+ }
63
+
64
+ try {
65
+ process.exitCode = main(process.argv.slice(2));
66
+ } catch (error) {
67
+ console.error(error.message);
68
+ process.exitCode = 2;
69
+ }