happy-stacks 0.6.12 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commit-audits/happy/_tools/generate-plans.mjs +453 -0
- package/docs/commit-audits/happy/_tools/generate-pr-assignment.mjs +430 -0
- package/docs/commit-audits/happy/_tools/init-pr-assignment-working.mjs +107 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +1849 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +747 -1
- package/docs/commit-audits/happy/leeroy-wip.commit-index.json +11740 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-index.tsv +252 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +18 -11
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1236 -92
- package/docs/commit-audits/happy/leeroy-wip.maintainers-overview.draft.md +448 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-assignment.draft.tsv +252 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-assignment.working.tsv +288 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-catalog.draft.md +245 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-stack-plan.draft.md +350 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-deferred-fragments.tsv +65 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-ledger.tsv +56 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-process.md +240 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-status.tsv +39 -0
- package/docs/commit-audits/happy/leeroy-wip.split-plan.draft.md +93 -0
- package/docs/commit-audits/happy/leeroy-wip.topic-buckets.md +76 -0
- package/docs/commit-audits/happy/pr-desc.extraction-ledger.tsv +279 -0
- package/docs/commit-audits/happy/pr-desc.original.md +0 -0
- package/docs/commit-audits/happy/pr-desc.post-audit-extraction-ledger.tsv +54 -0
- package/docs/commit-audits/happy/pr-desc.working-document.md +536 -0
- package/docs/happy-development.md +18 -1
- package/docs/isolated-linux-vm.md +23 -1
- package/docs/stacks.md +21 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +46 -8
- package/scripts/daemon.mjs +44 -21
- package/scripts/doctor.mjs +2 -2
- package/scripts/doctor_cmd.test.mjs +67 -0
- package/scripts/happy.mjs +18 -5
- package/scripts/provision/linux-ubuntu-review-pr.sh +5 -1
- package/scripts/provision/macos-lima-happy-vm.sh +34 -2
- package/scripts/review.mjs +347 -124
- package/scripts/review_pr.mjs +78 -2
- package/scripts/run.mjs +2 -1
- package/scripts/stack.mjs +265 -19
- package/scripts/stack_daemon_cmd.test.mjs +196 -0
- package/scripts/stack_happy_cmd.test.mjs +103 -0
- package/scripts/utils/cli/prereqs.mjs +12 -1
- package/scripts/utils/dev/daemon.mjs +3 -1
- package/scripts/utils/proc/pm.mjs +1 -1
- package/scripts/utils/review/detached_worktree.mjs +61 -0
- package/scripts/utils/review/detached_worktree.test.mjs +62 -0
- package/scripts/utils/review/findings.mjs +133 -20
- package/scripts/utils/review/findings.test.mjs +88 -1
- package/scripts/utils/review/runners/augment.mjs +71 -0
- package/scripts/utils/review/runners/augment.test.mjs +42 -0
- package/scripts/utils/review/runners/coderabbit.mjs +54 -10
- package/scripts/utils/review/runners/coderabbit.test.mjs +15 -48
- package/scripts/utils/review/sliced_runner.mjs +39 -0
- package/scripts/utils/review/sliced_runner.test.mjs +47 -0
- package/scripts/utils/review/tool_home_seed.mjs +99 -0
- package/scripts/utils/review/tool_home_seed.test.mjs +113 -0
- package/scripts/utils/stack/cli_identities.mjs +29 -0
- package/scripts/utils/stack/startup.mjs +45 -7
- package/scripts/worktrees.mjs +8 -5
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const ROOT = process.cwd();
|
|
5
|
+
const AUDIT_DIR = path.join(ROOT, 'docs', 'commit-audits', 'happy');
|
|
6
|
+
|
|
7
|
+
const INDEX_JSON = path.join(AUDIT_DIR, 'leeroy-wip.commit-index.json');
|
|
8
|
+
const OUT_TSV = path.join(AUDIT_DIR, 'leeroy-wip.pr-assignment.draft.tsv');
|
|
9
|
+
|
|
10
|
+
const PRS = [
|
|
11
|
+
{ id: 'PR01', title: 'Foundations / DevX' },
|
|
12
|
+
{ id: 'PR02', title: 'UI modal/overlay/popover + Expo web modal behavior' },
|
|
13
|
+
{ id: 'PR03', title: 'i18n foundation sweep (optional)' },
|
|
14
|
+
{ id: 'PR04', title: 'Auth + storage scoping' },
|
|
15
|
+
{ id: 'PR05', title: 'Persistence / drafts' },
|
|
16
|
+
{ id: 'PR06', title: 'Settings: API keys + experiments + settings screens' },
|
|
17
|
+
{ id: 'PR07', title: 'Env var templates + preview/resolution end-to-end' },
|
|
18
|
+
{ id: 'PR08', title: 'Profiles feature' },
|
|
19
|
+
{ id: 'PR09', title: 'Secrets / vault feature' },
|
|
20
|
+
{ id: 'PR10', title: 'Permission framework (Claude/cloud baseline)' },
|
|
21
|
+
{ id: 'PR11', title: 'Agent permissions (Codex/Gemini): permission modes + allowlists' },
|
|
22
|
+
{ id: 'PR12', title: 'AskUserQuestion + ExitPlan native handling' },
|
|
23
|
+
{ id: 'PR39', title: 'Tool UX: normalization + specialized views + tracing' },
|
|
24
|
+
{ id: 'PR13', title: 'MessageQueueV1 + Pending messages end-to-end' },
|
|
25
|
+
{ id: 'PR14', title: 'Capabilities end-to-end (protocol + wiring + reliability)' },
|
|
26
|
+
{ id: 'PR15', title: 'preview-env end-to-end' },
|
|
27
|
+
{ id: 'PR16', title: 'Daemon reliability (ownership + reattach/restart safety)' },
|
|
28
|
+
{ id: 'PR17', title: 'CLI reliability (waiters + reconnection + backoff semantics)' },
|
|
29
|
+
{ id: 'PR18', title: 'TMUX end-to-end (headless sessions + attach + settings)' },
|
|
30
|
+
{ id: 'PR19', title: 'Resume end-to-end' },
|
|
31
|
+
{ id: 'PR20', title: 'Claude session reliability (switching + transcript/scanner + local runner)' },
|
|
32
|
+
{ id: 'PR21', title: 'Codex MCP tool-call result correctness' },
|
|
33
|
+
{ id: 'PR22', title: 'Server-light flavor (variant + schema/prisma sync)' },
|
|
34
|
+
{ id: 'PR23', title: 'Server serves UI + public files safely' },
|
|
35
|
+
{ id: 'PR24', title: 'Storage S3 validation' },
|
|
36
|
+
{ id: 'PR25', title: 'Misc small UX polish (only if truly isolated)' },
|
|
37
|
+
{ id: 'PR26', title: 'New session wizard end-to-end' },
|
|
38
|
+
{ id: 'PR27', title: 'Sync robustness (transport/error parsing + backoff semantics)' },
|
|
39
|
+
{ id: 'PR28', title: 'Model modes (per-session selection + persistence)' },
|
|
40
|
+
{ id: 'PR29', title: 'Sessions & message list UX' },
|
|
41
|
+
{ id: 'PR30', title: 'Agent error surfaces (Codex/Gemini)' },
|
|
42
|
+
{ id: 'PR31', title: 'Terminal switching reliability (remote↔local)' },
|
|
43
|
+
{ id: 'PR32', title: 'Codex approvals + MCP tool interactions (execpolicy + elicitation)' },
|
|
44
|
+
{ id: 'PR33', title: 'Codex special commands (/clear session reset)' },
|
|
45
|
+
{ id: 'PR35', title: 'Friends: UX + reliability (search errors + follow-ups)' },
|
|
46
|
+
{ id: 'PR36', title: 'ACP agents end-to-end (runtimes + replay + tool normalization)' },
|
|
47
|
+
{ id: 'PR37', title: 'Agent registry end-to-end (selection + settings + UX helpers)' },
|
|
48
|
+
{ id: 'PR38', title: 'UI list primitives + pending UI polish' },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const PR_TITLE = new Map(PRS.map((p) => [p.id, p.title]));
|
|
52
|
+
|
|
53
|
+
function tsv(value) {
|
|
54
|
+
if (value == null) return '';
|
|
55
|
+
return String(value).replace(/\t/g, ' ').replace(/\r?\n/g, ' ');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function classify(commit) {
|
|
59
|
+
const subject = (commit.subject ?? '').toLowerCase();
|
|
60
|
+
const bucket = commit.topicBucket ?? 'unknown';
|
|
61
|
+
const scopeTag = commit.scopeTag ?? '';
|
|
62
|
+
const files = (commit.files ?? []).map((f) => f.path);
|
|
63
|
+
const anyFile = (re) => files.some((p) => re.test(p));
|
|
64
|
+
|
|
65
|
+
// Model modes (per-session selection/persistence)
|
|
66
|
+
if (subject.includes('model mode') || subject.includes('model modes') || anyFile(/modelMode|modelModes/i)) {
|
|
67
|
+
return { pr: 'PR28', confidence: 'high', reason: `model-modes keyword/file` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Foundations
|
|
71
|
+
if (['crypto', 'deps', 'config', 'format', 'typecheck', 'dev', 'runtime', 'logging', 'test'].includes(bucket)) {
|
|
72
|
+
return { pr: 'PR01', confidence: 'high', reason: `bucket=${bucket}` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ACP agents (CLI/runtime + replay + normalization)
|
|
76
|
+
if (bucket === 'acp') {
|
|
77
|
+
return { pr: 'PR36', confidence: 'high', reason: `bucket=${bucket}` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Agent registry (Expo app)
|
|
81
|
+
if (bucket === 'agents') {
|
|
82
|
+
return { pr: 'PR37', confidence: 'high', reason: `bucket=${bucket}` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// UI list primitives / pending UI polish
|
|
86
|
+
if (bucket === 'list') {
|
|
87
|
+
return { pr: 'PR38', confidence: 'high', reason: `bucket=${bucket}` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// i18n (optional early sweep) — mark medium so we can decide to fold later.
|
|
91
|
+
if (bucket === 'i18n' || subject.includes('localize') || subject.includes('translate')) {
|
|
92
|
+
return { pr: 'PR03', confidence: 'medium', reason: `i18n bucket/keyword` };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Auth + storage scoping
|
|
96
|
+
if (bucket === 'auth') {
|
|
97
|
+
return { pr: 'PR04', confidence: 'high', reason: `auth bucket` };
|
|
98
|
+
}
|
|
99
|
+
if (bucket === 'persistence') {
|
|
100
|
+
return { pr: 'PR05', confidence: 'high', reason: `persistence bucket` };
|
|
101
|
+
}
|
|
102
|
+
if (bucket === 'settings') {
|
|
103
|
+
return { pr: 'PR06', confidence: 'medium', reason: `settings bucket` };
|
|
104
|
+
}
|
|
105
|
+
if (bucket === 'env') {
|
|
106
|
+
return { pr: 'PR07', confidence: 'high', reason: `env bucket` };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// New session wizard feature
|
|
110
|
+
if (bucket === 'new-session' || bucket === 'new' || bucket === 'machine') {
|
|
111
|
+
return { pr: 'PR26', confidence: 'high', reason: `new-session/new/machine bucket` };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Expo-router / web modals / postinstall patching
|
|
115
|
+
if (bucket === 'expo-app' || bucket === 'expo') {
|
|
116
|
+
return { pr: 'PR02', confidence: 'medium', reason: `expo bucket` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Agent error surfaces (Codex/Gemini) — keep separate from modal/overlay primitives.
|
|
120
|
+
if (
|
|
121
|
+
(subject.includes('codex') && subject.includes('error')) ||
|
|
122
|
+
(subject.includes('gemini') && subject.includes('error')) ||
|
|
123
|
+
anyFile(/formatCodexEventForUi|formatGeminiErrorForUi|formatErrorForUi/i)
|
|
124
|
+
) {
|
|
125
|
+
return { pr: 'PR30', confidence: 'medium', reason: `agent error surfaces keyword/file` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Codex approvals + MCP tool interactions (execpolicy/elicitation/tool-call UX)
|
|
129
|
+
if (
|
|
130
|
+
subject.includes('execpolicy') ||
|
|
131
|
+
subject.includes('elicitation') ||
|
|
132
|
+
subject.includes('exec policy') ||
|
|
133
|
+
subject.includes('mcp tool call')
|
|
134
|
+
) {
|
|
135
|
+
return { pr: 'PR32', confidence: 'medium', reason: `codex approval/mcp keyword` };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// MCP bridge runner compatibility (bun/node) — keep with Codex approvals + MCP tool interactions
|
|
139
|
+
if (subject.includes('mcp bridge') || anyFile(/bin\/happy-mcp\.mjs/i)) {
|
|
140
|
+
return { pr: 'PR32', confidence: 'high', reason: `mcp bridge keyword/file` };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// UI primitives + nav/UX surfaces (including a11y)
|
|
144
|
+
if (['ui', 'modal', 'popover', 'agent-input', 'web', 'a11y', 'command-palette', 'autocomplete', 'experiments', 'zen'].includes(bucket)) {
|
|
145
|
+
return { pr: 'PR02', confidence: 'medium', reason: `ui-system bucket=${bucket}` };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Friends search UX (stable error codes + localized failures)
|
|
149
|
+
if (subject.includes('search error') || anyFile(/hooks\/useSearch\.ts|friends\/search\.tsx/i)) {
|
|
150
|
+
return { pr: 'PR35', confidence: 'high', reason: `search keyword/file` };
|
|
151
|
+
}
|
|
152
|
+
if (bucket === 'friends' || bucket === 'inbox') {
|
|
153
|
+
return { pr: 'PR35', confidence: 'high', reason: `bucket=${bucket}` };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Sessions list UX
|
|
157
|
+
if (bucket === 'sessions') {
|
|
158
|
+
return { pr: 'PR29', confidence: 'high', reason: `sessions bucket` };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Resume UX / machine installer surfaces (avoid routing to generic UI primitives)
|
|
162
|
+
if (subject.includes('codex resume') || subject.includes('inactive resume') || subject.includes('resume session') || subject.includes('resume-server')) {
|
|
163
|
+
return { pr: 'PR19', confidence: 'medium', reason: `resume keyword` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Capabilities UI tests / migration
|
|
167
|
+
if (subject.includes('useclidetection') || subject.includes('capabilities snapshot')) {
|
|
168
|
+
return { pr: 'PR14', confidence: 'medium', reason: `capabilities UI keyword` };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Profiles
|
|
172
|
+
if (bucket === 'profiles' || bucket === 'api-keys') {
|
|
173
|
+
return { pr: 'PR08', confidence: 'high', reason: `profiles/api-keys bucket` };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Secrets
|
|
177
|
+
if (bucket === 'secrets' || subject.includes('secret') || subject.includes('vault')) {
|
|
178
|
+
return { pr: 'PR09', confidence: 'medium', reason: `secrets keyword/bucket` };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Permissions
|
|
182
|
+
if (bucket === 'permission') {
|
|
183
|
+
return { pr: 'PR10', confidence: 'medium', reason: `permission bucket` };
|
|
184
|
+
}
|
|
185
|
+
if (subject.includes('permission mode for codex') || subject.includes('gemini permission') || subject.includes('codex permission')) {
|
|
186
|
+
return { pr: 'PR11', confidence: 'medium', reason: `agent-specific permission keyword` };
|
|
187
|
+
}
|
|
188
|
+
if (subject.includes('permission mode')) {
|
|
189
|
+
if (bucket === 'claude') return { pr: 'PR10', confidence: 'medium', reason: `claude permission-mode keyword` };
|
|
190
|
+
if (bucket === 'codex' || bucket === 'gemini') return { pr: 'PR11', confidence: 'medium', reason: `agent-specific permission-mode keyword` };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Tools
|
|
194
|
+
if (bucket === 'tools' || subject.includes('askuserquestion') || subject.includes('exitplan') || subject.includes('tool')) {
|
|
195
|
+
// AskUserQuestion / ExitPlan are the protocol/interaction feature (keep separate from generic tool rendering).
|
|
196
|
+
if (subject.includes('askuserquestion') || subject.includes('exitplan')) {
|
|
197
|
+
return { pr: 'PR12', confidence: 'high', reason: `askuserquestion/exitplan keyword` };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Everything else under tools is a tool-UX slice: normalization, specialized views, tracing, fixtures.
|
|
201
|
+
if (subject.includes('tool-trace') || subject.includes('tool trace') || subject.includes('normalize')) {
|
|
202
|
+
return { pr: 'PR39', confidence: 'high', reason: `tool tracing/normalization keyword` };
|
|
203
|
+
}
|
|
204
|
+
if (anyFile(/\/toolTrace\//i) || anyFile(/components\/tools\//i)) {
|
|
205
|
+
return { pr: 'PR39', confidence: 'high', reason: `tool UX files` };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { pr: 'PR39', confidence: 'medium', reason: `tools bucket fallback` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Queue/pending
|
|
212
|
+
if (bucket === 'queue' || (bucket === 'sync' && subject.includes('messagequeue'))) {
|
|
213
|
+
return { pr: 'PR13', confidence: 'high', reason: `queue bucket/keyword` };
|
|
214
|
+
}
|
|
215
|
+
if (subject.includes('pending message') || subject.includes('messagequeuev1')) {
|
|
216
|
+
return { pr: 'PR13', confidence: 'medium', reason: `pending/queue keyword` };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Sync robustness (non-feature-specific)
|
|
220
|
+
if (bucket === 'sync') {
|
|
221
|
+
if (subject.includes('permission mode')) return { pr: 'PR10', confidence: 'medium', reason: `sync permission mode` };
|
|
222
|
+
if (subject.includes('invalidate') || subject.includes('json') || subject.includes('disconnect') || subject.includes('backoff')) {
|
|
223
|
+
return { pr: 'PR27', confidence: 'medium', reason: `sync robustness keyword` };
|
|
224
|
+
}
|
|
225
|
+
// Fall back to PR27; we can later move items into feature PRs when mapping precisely.
|
|
226
|
+
return { pr: 'PR27', confidence: 'low', reason: `sync bucket fallback` };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Capabilities
|
|
230
|
+
if (bucket === 'cli-detection' || bucket === 'detect-cli' || subject.includes('capabilities')) {
|
|
231
|
+
return { pr: 'PR14', confidence: 'high', reason: `capabilities bucket/keyword` };
|
|
232
|
+
}
|
|
233
|
+
if (bucket === 'hooks' && (subject.includes('capabilities') || subject.includes('useclidetection') || subject.includes('cache'))) {
|
|
234
|
+
return { pr: 'PR14', confidence: 'medium', reason: `capabilities-related hooks` };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// preview-env
|
|
238
|
+
if (subject.includes('preview-env') || anyFile(/previewEnv/i)) {
|
|
239
|
+
return { pr: 'PR15', confidence: 'high', reason: `preview-env keyword/file` };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Daemon reliability
|
|
243
|
+
if (bucket === 'daemon' || bucket === 'pr107') {
|
|
244
|
+
return { pr: 'PR16', confidence: 'medium', reason: `daemon bucket` };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Terminal switching / Ink reliability (non-tmux-specific)
|
|
248
|
+
if (
|
|
249
|
+
bucket === 'ink' ||
|
|
250
|
+
(bucket === 'terminal' && (subject.includes('switch') || subject.includes('stdin') || subject.includes('remote') || subject.includes('signal'))) ||
|
|
251
|
+
(subject.includes('forward') && subject.includes('signal'))
|
|
252
|
+
) {
|
|
253
|
+
return { pr: 'PR31', confidence: 'medium', reason: `terminal switching keyword/bucket` };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// CLI reliability (broad)
|
|
257
|
+
if (bucket === 'utils' || bucket === 'common' || bucket === 'happy-cli') {
|
|
258
|
+
return { pr: 'PR17', confidence: 'medium', reason: `cli reliability bucket=${bucket}` };
|
|
259
|
+
}
|
|
260
|
+
if (bucket === 'cli' || bucket === 'rpc' || bucket === 'socket' || bucket === 'reducer' || bucket === 'app') {
|
|
261
|
+
// broad; attempt to route some to capabilities/queue/tmux/resume based on paths
|
|
262
|
+
if (anyFile(/terminal/i) || anyFile(/tmux/i)) return { pr: 'PR18', confidence: 'medium', reason: `terminal/tmux files` };
|
|
263
|
+
if (anyFile(/messageQueueV1/i) || subject.includes('messagequeue')) return { pr: 'PR13', confidence: 'medium', reason: `queue files` };
|
|
264
|
+
if (subject.includes('resume') || bucket === 'resume') return { pr: 'PR19', confidence: 'medium', reason: `resume keyword/bucket` };
|
|
265
|
+
if (subject.includes('capabilities')) return { pr: 'PR14', confidence: 'medium', reason: `capabilities keyword` };
|
|
266
|
+
return { pr: 'PR17', confidence: 'low', reason: `fallback cli/rpc/app bucket=${bucket}` };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// TMUX/Terminal
|
|
270
|
+
if (bucket === 'terminal' || bucket === 'tmux') {
|
|
271
|
+
return { pr: 'PR18', confidence: 'high', reason: `terminal/tmux/ink bucket` };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Resume
|
|
275
|
+
if (bucket === 'resume' || bucket === 'session' || bucket === 'sessions' || bucket === 'scanner' || bucket === 'offline') {
|
|
276
|
+
return { pr: 'PR19', confidence: 'high', reason: `resume/session bucket` };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Claude transcript scanning
|
|
280
|
+
if (bucket === 'claude' && (subject.includes('scanner') || subject.includes('transcript'))) {
|
|
281
|
+
return { pr: 'PR20', confidence: 'medium', reason: `claude scanner/transcript keyword` };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Claude (non-scanner): usually terminal/remote-local switching or resume-related; route by keywords.
|
|
285
|
+
if (bucket === 'claude') {
|
|
286
|
+
if (subject.includes('switch') || subject.includes('remote') || subject.includes('local')) {
|
|
287
|
+
return { pr: 'PR20', confidence: 'medium', reason: `claude remote/local switching` };
|
|
288
|
+
}
|
|
289
|
+
return { pr: 'PR19', confidence: 'low', reason: `claude fallback to resume` };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Codex tool-call result correctness
|
|
293
|
+
if (bucket === 'codex' && subject.includes('mcp tool')) {
|
|
294
|
+
return { pr: 'PR21', confidence: 'medium', reason: `codex mcp keyword` };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Codex special commands (/clear, etc)
|
|
298
|
+
if (
|
|
299
|
+
bucket === 'codex' &&
|
|
300
|
+
(subject.includes('/clear') || subject.includes('special command') || subject.includes('session reset'))
|
|
301
|
+
) {
|
|
302
|
+
return { pr: 'PR33', confidence: 'high', reason: `codex special command keyword` };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Codex (non tool-call-result): treat as part of resume/installer feature.
|
|
306
|
+
if (bucket === 'codex' || bucket === 'gemini') {
|
|
307
|
+
if (subject.includes('permission')) return { pr: 'PR11', confidence: 'low', reason: `codex/gemini permission keyword` };
|
|
308
|
+
if (subject.includes('error') || subject.includes('format')) return { pr: 'PR30', confidence: 'low', reason: `agent error surfaces keyword` };
|
|
309
|
+
return { pr: 'PR19', confidence: 'low', reason: `codex/gemini fallback to resume` };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Previously-tagged fork commits must be upstreamed per decision; treat them as resume.
|
|
313
|
+
if (bucket === 'fork') {
|
|
314
|
+
return { pr: 'PR19', confidence: 'medium', reason: `fork-tagged commit routed to resume for upstreaming` };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Server-light
|
|
318
|
+
if (bucket === 'server-light' || subject.includes('server-light')) {
|
|
319
|
+
return { pr: 'PR22', confidence: 'high', reason: `server-light bucket/keyword` };
|
|
320
|
+
}
|
|
321
|
+
if (bucket === 'server') {
|
|
322
|
+
if (subject.includes('light') || subject.includes('sqlite') || subject.includes('schema') || subject.includes('prisma')) {
|
|
323
|
+
return { pr: 'PR22', confidence: 'medium', reason: `server light/schema/prisma keyword` };
|
|
324
|
+
}
|
|
325
|
+
if (subject.includes('ui') || subject.includes('public file') || subject.includes('index')) {
|
|
326
|
+
return { pr: 'PR23', confidence: 'medium', reason: `server serves UI/public files` };
|
|
327
|
+
}
|
|
328
|
+
// Misc server tweaks that are primarily typecheck/test infra can be treated as foundations.
|
|
329
|
+
if (subject.includes('typecheck') || subject.includes('tsconfig') || subject.includes('test(')) {
|
|
330
|
+
return { pr: 'PR01', confidence: 'low', reason: `server infra/test keyword` };
|
|
331
|
+
}
|
|
332
|
+
return { pr: 'PR23', confidence: 'low', reason: `server fallback` };
|
|
333
|
+
}
|
|
334
|
+
// Legacy per-file heuristics for server (kept for safety if bucket tagging is inconsistent).
|
|
335
|
+
if (anyFile(/schemaSync|enums\.generated|storage\/prisma/i)) {
|
|
336
|
+
return { pr: 'PR22', confidence: 'medium', reason: `schema/prisma files` };
|
|
337
|
+
}
|
|
338
|
+
if (anyFile(/flavors\/light\/files|enableErrorHandlers/i)) {
|
|
339
|
+
return { pr: 'PR23', confidence: 'medium', reason: `serve UI/public files (by file)` };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Storage S3
|
|
343
|
+
if (bucket === 'storage' || anyFile(/server\/sources\/storage\/files\./i)) {
|
|
344
|
+
return { pr: 'PR24', confidence: 'high', reason: `storage bucket/file` };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// happy bucket is mostly capabilities/hook plumbing or test/dev scaffolding.
|
|
348
|
+
if (bucket === 'happy') {
|
|
349
|
+
if (subject.includes('useclidetection') || subject.includes('capabilities') || anyFile(/useCLIDetection|useMachineCapabilitiesCache/i)) {
|
|
350
|
+
return { pr: 'PR14', confidence: 'medium', reason: `happy capabilities/hook keyword` };
|
|
351
|
+
}
|
|
352
|
+
return { pr: 'PR01', confidence: 'low', reason: `happy bucket fallback` };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Default: this is a signal to refine mapping further (ideally PR25 ends empty or very small).
|
|
356
|
+
return { pr: 'PR25', confidence: 'low', reason: `unclassified bucket=${bucket}` };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function main() {
|
|
360
|
+
const raw = JSON.parse(await fs.readFile(INDEX_JSON, 'utf8'));
|
|
361
|
+
const commits = raw.commits ?? [];
|
|
362
|
+
|
|
363
|
+
const rows = [];
|
|
364
|
+
for (const c of commits) {
|
|
365
|
+
const { pr, confidence, reason } = classify(c);
|
|
366
|
+
rows.push({
|
|
367
|
+
n: String(c.n).padStart(3, '0'),
|
|
368
|
+
shaShort: c.shaShort ?? '',
|
|
369
|
+
topicBucket: c.topicBucket ?? '',
|
|
370
|
+
scopeTag: c.scopeTag ?? '',
|
|
371
|
+
subject: c.subject ?? '',
|
|
372
|
+
verdict: c.verdictKind ?? '',
|
|
373
|
+
filesChanged: c.filesChanged ?? '',
|
|
374
|
+
pr,
|
|
375
|
+
prTitle: PR_TITLE.get(pr) ?? '',
|
|
376
|
+
confidence,
|
|
377
|
+
reason,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const header = [
|
|
382
|
+
'n',
|
|
383
|
+
'shaShort',
|
|
384
|
+
'topicBucket',
|
|
385
|
+
'scopeTag',
|
|
386
|
+
'subject',
|
|
387
|
+
'verdict',
|
|
388
|
+
'filesChanged',
|
|
389
|
+
'proposedPr',
|
|
390
|
+
'proposedPrTitle',
|
|
391
|
+
'confidence',
|
|
392
|
+
'reason',
|
|
393
|
+
'notes',
|
|
394
|
+
].join('\t');
|
|
395
|
+
|
|
396
|
+
const out = [
|
|
397
|
+
'# GENERATED FILE - do not edit by hand.',
|
|
398
|
+
'# Regenerate: node docs/commit-audits/happy/_tools/generate-pr-assignment.mjs',
|
|
399
|
+
header,
|
|
400
|
+
...rows.map((r) =>
|
|
401
|
+
[
|
|
402
|
+
r.n,
|
|
403
|
+
r.shaShort,
|
|
404
|
+
r.topicBucket,
|
|
405
|
+
r.scopeTag,
|
|
406
|
+
r.subject,
|
|
407
|
+
r.verdict,
|
|
408
|
+
r.filesChanged,
|
|
409
|
+
r.pr,
|
|
410
|
+
r.prTitle,
|
|
411
|
+
r.confidence,
|
|
412
|
+
r.reason,
|
|
413
|
+
'',
|
|
414
|
+
]
|
|
415
|
+
.map(tsv)
|
|
416
|
+
.join('\t'),
|
|
417
|
+
),
|
|
418
|
+
'',
|
|
419
|
+
].join('\n');
|
|
420
|
+
|
|
421
|
+
await fs.writeFile(OUT_TSV, out, 'utf8');
|
|
422
|
+
// eslint-disable-next-line no-console
|
|
423
|
+
console.log(`[audit] wrote: ${path.relative(ROOT, OUT_TSV)}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
main().catch((err) => {
|
|
427
|
+
// eslint-disable-next-line no-console
|
|
428
|
+
console.error(err);
|
|
429
|
+
process.exitCode = 1;
|
|
430
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const ROOT = process.cwd();
|
|
5
|
+
const AUDIT_DIR = path.join(ROOT, 'docs', 'commit-audits', 'happy');
|
|
6
|
+
|
|
7
|
+
const DRAFT_TSV = path.join(AUDIT_DIR, 'leeroy-wip.pr-assignment.draft.tsv');
|
|
8
|
+
const WORKING_TSV = path.join(AUDIT_DIR, 'leeroy-wip.pr-assignment.working.tsv');
|
|
9
|
+
|
|
10
|
+
function tsv(value) {
|
|
11
|
+
if (value == null) return '';
|
|
12
|
+
return String(value).replace(/\t/g, ' ').replace(/\r?\n/g, ' ');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseTsvLines(text) {
|
|
16
|
+
const lines = text.split('\n').filter((l) => l.trim().length > 0);
|
|
17
|
+
const dataLines = lines.filter((l) => !l.startsWith('#'));
|
|
18
|
+
if (dataLines.length < 2) {
|
|
19
|
+
throw new Error('Draft TSV is missing header/rows.');
|
|
20
|
+
}
|
|
21
|
+
const header = dataLines[0].split('\t');
|
|
22
|
+
const rows = dataLines.slice(1).map((l) => l.split('\t'));
|
|
23
|
+
return { header, rows };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
const args = new Set(process.argv.slice(2));
|
|
28
|
+
const force = args.has('--force');
|
|
29
|
+
|
|
30
|
+
if (!force) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(WORKING_TSV);
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log(`[audit] exists: ${path.relative(ROOT, WORKING_TSV)} (use --force to overwrite)`);
|
|
35
|
+
return;
|
|
36
|
+
} catch {
|
|
37
|
+
// continue
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const draft = await fs.readFile(DRAFT_TSV, 'utf8');
|
|
42
|
+
const { header, rows } = parseTsvLines(draft);
|
|
43
|
+
|
|
44
|
+
const colIndex = new Map(header.map((h, i) => [h, i]));
|
|
45
|
+
const required = ['n', 'shaShort', 'topicBucket', 'scopeTag', 'subject', 'verdict', 'filesChanged', 'proposedPr', 'proposedPrTitle'];
|
|
46
|
+
for (const r of required) {
|
|
47
|
+
if (!colIndex.has(r)) throw new Error(`Draft TSV missing column: ${r}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const outHeader = [
|
|
51
|
+
...header,
|
|
52
|
+
'finalPr',
|
|
53
|
+
'finalTitle',
|
|
54
|
+
'needsSplit',
|
|
55
|
+
'translationHandling',
|
|
56
|
+
'manualReviewedForPrPlan',
|
|
57
|
+
'plannedAt',
|
|
58
|
+
'plannerNotes',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const outRows = rows.map((r) => {
|
|
62
|
+
const proposedPr = r[colIndex.get('proposedPr')];
|
|
63
|
+
const proposedTitle = r[colIndex.get('proposedPrTitle')];
|
|
64
|
+
const verdict = (r[colIndex.get('verdict')] ?? '').toLowerCase();
|
|
65
|
+
const needsSplit = verdict === 'split' ? 'yes' : '';
|
|
66
|
+
|
|
67
|
+
const topicBucket = (r[colIndex.get('topicBucket')] ?? '').toLowerCase();
|
|
68
|
+
const translationHandling = topicBucket === 'i18n' ? 'move-to-feature-pr' : 'in-feature-pr';
|
|
69
|
+
|
|
70
|
+
// We initialize finalPr/title to the proposal; manual review will override.
|
|
71
|
+
return [
|
|
72
|
+
...r,
|
|
73
|
+
proposedPr,
|
|
74
|
+
proposedTitle,
|
|
75
|
+
needsSplit,
|
|
76
|
+
translationHandling,
|
|
77
|
+
'no',
|
|
78
|
+
'',
|
|
79
|
+
'',
|
|
80
|
+
].map(tsv);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const out = [
|
|
84
|
+
'# WORKING FILE - safe to edit by hand.',
|
|
85
|
+
'# Seeded from: leeroy-wip.pr-assignment.draft.tsv',
|
|
86
|
+
'# Regenerate seed (overwrites): node docs/commit-audits/happy/_tools/init-pr-assignment-working.mjs --force',
|
|
87
|
+
'# Guidance:',
|
|
88
|
+
'# - Set finalPr/finalTitle per commit after reading its full manual-review section.',
|
|
89
|
+
'# - Set manualReviewedForPrPlan=yes only after you have read the entire section for that commit in leeroy-wip.commit-manual-review.md.',
|
|
90
|
+
'# - Keep translationHandling=in-feature-pr unless you explicitly decide otherwise for a specific commit.',
|
|
91
|
+
'',
|
|
92
|
+
outHeader.join('\t'),
|
|
93
|
+
...outRows.map((cols) => cols.join('\t')),
|
|
94
|
+
'',
|
|
95
|
+
].join('\n');
|
|
96
|
+
|
|
97
|
+
await fs.writeFile(WORKING_TSV, out, 'utf8');
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.log(`[audit] wrote: ${path.relative(ROOT, WORKING_TSV)}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main().catch((err) => {
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.error(err);
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
});
|
|
107
|
+
|