happy-stacks 0.6.11 → 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.
Files changed (66) hide show
  1. package/docs/commit-audits/happy/_tools/generate-plans.mjs +453 -0
  2. package/docs/commit-audits/happy/_tools/generate-pr-assignment.mjs +430 -0
  3. package/docs/commit-audits/happy/_tools/init-pr-assignment-working.mjs +107 -0
  4. package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +1849 -0
  5. package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +747 -1
  6. package/docs/commit-audits/happy/leeroy-wip.commit-index.json +11740 -0
  7. package/docs/commit-audits/happy/leeroy-wip.commit-index.tsv +252 -0
  8. package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +18 -11
  9. package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1297 -140
  10. package/docs/commit-audits/happy/leeroy-wip.maintainers-overview.draft.md +448 -0
  11. package/docs/commit-audits/happy/leeroy-wip.pr-assignment.draft.tsv +252 -0
  12. package/docs/commit-audits/happy/leeroy-wip.pr-assignment.working.tsv +288 -0
  13. package/docs/commit-audits/happy/leeroy-wip.pr-catalog.draft.md +245 -0
  14. package/docs/commit-audits/happy/leeroy-wip.pr-stack-plan.draft.md +350 -0
  15. package/docs/commit-audits/happy/leeroy-wip.rewrite-deferred-fragments.tsv +65 -0
  16. package/docs/commit-audits/happy/leeroy-wip.rewrite-ledger.tsv +56 -0
  17. package/docs/commit-audits/happy/leeroy-wip.rewrite-process.md +240 -0
  18. package/docs/commit-audits/happy/leeroy-wip.rewrite-status.tsv +39 -0
  19. package/docs/commit-audits/happy/leeroy-wip.split-plan.draft.md +93 -0
  20. package/docs/commit-audits/happy/leeroy-wip.topic-buckets.md +76 -0
  21. package/docs/commit-audits/happy/pr-desc.extraction-ledger.tsv +279 -0
  22. package/docs/commit-audits/happy/pr-desc.original.md +0 -0
  23. package/docs/commit-audits/happy/pr-desc.post-audit-extraction-ledger.tsv +54 -0
  24. package/docs/commit-audits/happy/pr-desc.working-document.md +536 -0
  25. package/docs/happy-development.md +18 -1
  26. package/docs/isolated-linux-vm.md +23 -1
  27. package/docs/stacks.md +21 -1
  28. package/package.json +1 -1
  29. package/scripts/auth.mjs +46 -8
  30. package/scripts/daemon.mjs +44 -21
  31. package/scripts/doctor.mjs +2 -2
  32. package/scripts/doctor_cmd.test.mjs +67 -0
  33. package/scripts/happy.mjs +18 -5
  34. package/scripts/import.mjs +19 -13
  35. package/scripts/monorepo.mjs +414 -23
  36. package/scripts/monorepo_port.test.mjs +136 -0
  37. package/scripts/provision/linux-ubuntu-review-pr.sh +5 -1
  38. package/scripts/provision/macos-lima-happy-vm.sh +34 -2
  39. package/scripts/review.mjs +347 -124
  40. package/scripts/review_pr.mjs +78 -2
  41. package/scripts/run.mjs +2 -1
  42. package/scripts/stack.mjs +265 -19
  43. package/scripts/stack_daemon_cmd.test.mjs +196 -0
  44. package/scripts/stack_happy_cmd.test.mjs +103 -0
  45. package/scripts/utils/cli/prereqs.mjs +12 -1
  46. package/scripts/utils/cli/wizard.mjs +19 -1
  47. package/scripts/utils/dev/daemon.mjs +3 -1
  48. package/scripts/utils/llm/assist.mjs +64 -31
  49. package/scripts/utils/llm/happys_runner.mjs +60 -0
  50. package/scripts/utils/llm/tools.mjs +3 -3
  51. package/scripts/utils/proc/pm.mjs +1 -1
  52. package/scripts/utils/review/detached_worktree.mjs +61 -0
  53. package/scripts/utils/review/detached_worktree.test.mjs +62 -0
  54. package/scripts/utils/review/findings.mjs +133 -20
  55. package/scripts/utils/review/findings.test.mjs +88 -1
  56. package/scripts/utils/review/runners/augment.mjs +71 -0
  57. package/scripts/utils/review/runners/augment.test.mjs +42 -0
  58. package/scripts/utils/review/runners/coderabbit.mjs +54 -10
  59. package/scripts/utils/review/runners/coderabbit.test.mjs +15 -48
  60. package/scripts/utils/review/sliced_runner.mjs +39 -0
  61. package/scripts/utils/review/sliced_runner.test.mjs +47 -0
  62. package/scripts/utils/review/tool_home_seed.mjs +99 -0
  63. package/scripts/utils/review/tool_home_seed.test.mjs +113 -0
  64. package/scripts/utils/stack/cli_identities.mjs +29 -0
  65. package/scripts/utils/stack/startup.mjs +45 -7
  66. package/scripts/worktrees.mjs +8 -5
@@ -275,6 +275,76 @@ test('monorepo port can clone the target monorepo into a new directory', async (
275
275
  assert.equal(content, 'v2\n');
276
276
  });
277
277
 
278
+ test('monorepo port guide auto-clones target when --target does not exist', async (t) => {
279
+ const root = await withTempRoot(t);
280
+ const seedMono = join(root, 'seed-mono');
281
+ const target = join(root, 'target-guide-autoclone'); // does not exist yet
282
+ const sourceCli = join(root, 'source-cli');
283
+ const env = gitEnv();
284
+
285
+ // Seed monorepo repo that will be cloned into `target`
286
+ await initMonorepoStub({ dir: seedMono, env, seed: { 'cli/hello.txt': 'v1\n' } });
287
+
288
+ // Source CLI repo with one change commit (v1 -> v2)
289
+ const base = await initSplitRepoStub({ dir: sourceCli, env, name: 'cli', seed: { 'hello.txt': 'v1\n' } });
290
+ await writeFile(join(sourceCli, 'hello.txt'), 'v2\n', 'utf-8');
291
+ await run('git', ['add', '.'], { cwd: sourceCli, env });
292
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env });
293
+
294
+ // Guide requires a TTY, but with all args provided it should not prompt.
295
+ // We spawn so the guide sees a TTY (required), but still feed no input.
296
+ const child = spawn(
297
+ process.execPath,
298
+ [
299
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
300
+ 'port',
301
+ 'guide',
302
+ `--target=${target}`,
303
+ `--target-repo=${seedMono}`,
304
+ '--branch=port/test-guide-autoclone',
305
+ '--3way',
306
+ `--from-happy-cli=${sourceCli}`,
307
+ `--from-happy-cli-base=${base}`,
308
+ '--json',
309
+ ],
310
+ {
311
+ cwd: process.cwd(),
312
+ env: { ...env, HAPPY_STACKS_TEST_TTY: '1', HAPPY_STACKS_DISABLE_LLM_AUTOEXEC: '1' },
313
+ stdio: ['pipe', 'pipe', 'pipe'],
314
+ }
315
+ );
316
+ t.after(() => {
317
+ try {
318
+ child.kill('SIGKILL');
319
+ } catch {
320
+ // ignore
321
+ }
322
+ });
323
+ let out = '';
324
+ let err = '';
325
+ let exitCode = null;
326
+ child.stdout?.on('data', (d) => (out += d.toString()));
327
+ child.stderr?.on('data', (d) => (err += d.toString()));
328
+ child.on('exit', (code) => {
329
+ exitCode = code;
330
+ });
331
+
332
+ const waitForExit = async (timeoutMs) => {
333
+ const started = Date.now();
334
+ while (Date.now() - started < timeoutMs) {
335
+ if (exitCode != null) return;
336
+ // eslint-disable-next-line no-await-in-loop
337
+ await new Promise((r) => setTimeout(r, 20));
338
+ }
339
+ throw new Error(`timeout waiting for guide to exit\nstdout:\n${out}\nstderr:\n${err}`);
340
+ };
341
+ await waitForExit(20_000);
342
+ assert.equal(exitCode, 0, `expected guide to exit 0\nstdout:\n${out}\nstderr:\n${err}`);
343
+
344
+ const content = (await readFile(join(target, 'cli', 'hello.txt'), 'utf-8')).toString();
345
+ assert.equal(content, 'v2\n');
346
+ });
347
+
278
348
  test('monorepo port accepts source repo URLs by cloning them into a temp checkout', async (t) => {
279
349
  const root = await withTempRoot(t);
280
350
  const target = join(root, 'target-mono');
@@ -721,6 +791,72 @@ test('monorepo port continue runs git am --continue after conflicts are resolved
721
791
  assert.equal(content, 'value=source\n');
722
792
  });
723
793
 
794
+ test('monorepo port continue --stage stages conflicted files before continuing', async (t) => {
795
+ const root = await withTempRoot(t);
796
+ const target = join(root, 'target-mono');
797
+ const sourceCli = join(root, 'source-cli');
798
+
799
+ // Target monorepo stub with cli/hello.txt="value=target".
800
+ await mkdir(target, { recursive: true });
801
+ await run('git', ['init', '-q'], { cwd: target, env: gitEnv() });
802
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: target, env: gitEnv() });
803
+ await mkdir(join(target, 'expo-app'), { recursive: true });
804
+ await mkdir(join(target, 'cli'), { recursive: true });
805
+ await mkdir(join(target, 'server'), { recursive: true });
806
+ await writeFile(join(target, 'expo-app', 'package.json'), '{}\n', 'utf-8');
807
+ await writeFile(join(target, 'cli', 'package.json'), '{}\n', 'utf-8');
808
+ await writeFile(join(target, 'server', 'package.json'), '{}\n', 'utf-8');
809
+ await writeFile(join(target, 'cli', 'hello.txt'), 'value=target\n', 'utf-8');
810
+ await run('git', ['add', '.'], { cwd: target, env: gitEnv() });
811
+ await run('git', ['commit', '-q', '-m', 'chore: init monorepo'], { cwd: target, env: gitEnv() });
812
+
813
+ // Source CLI repo with base="value=base" and a commit changing to "value=source".
814
+ await mkdir(sourceCli, { recursive: true });
815
+ await run('git', ['init', '-q'], { cwd: sourceCli, env: gitEnv() });
816
+ await run('git', ['checkout', '-q', '-b', 'main'], { cwd: sourceCli, env: gitEnv() });
817
+ await writeFile(join(sourceCli, 'package.json'), '{}\n', 'utf-8');
818
+ await writeFile(join(sourceCli, 'hello.txt'), 'value=base\n', 'utf-8');
819
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
820
+ await run('git', ['commit', '-q', '-m', 'chore: init cli'], { cwd: sourceCli, env: gitEnv() });
821
+ const base = (await runCapture('git', ['rev-parse', 'HEAD'], { cwd: sourceCli, env: gitEnv() })).trim();
822
+ await writeFile(join(sourceCli, 'hello.txt'), 'value=source\n', 'utf-8');
823
+ await run('git', ['add', '.'], { cwd: sourceCli, env: gitEnv() });
824
+ await run('git', ['commit', '-q', '-m', 'feat: update hello'], { cwd: sourceCli, env: gitEnv() });
825
+
826
+ // Start a port that will stop with an am conflict.
827
+ await assert.rejects(
828
+ async () =>
829
+ await runCapture(
830
+ process.execPath,
831
+ [
832
+ join(process.cwd(), 'scripts', 'monorepo.mjs'),
833
+ 'port',
834
+ `--target=${target}`,
835
+ `--branch=port/test-continue-stage`,
836
+ `--from-happy-cli=${sourceCli}`,
837
+ `--from-happy-cli-base=${base}`,
838
+ '--3way',
839
+ ],
840
+ { cwd: process.cwd(), env: gitEnv() }
841
+ )
842
+ );
843
+
844
+ // Resolve the conflict by choosing "value=source", but DO NOT stage it.
845
+ await writeFile(join(target, 'cli', 'hello.txt'), 'value=source\n', 'utf-8');
846
+
847
+ const out = await runCapture(
848
+ process.execPath,
849
+ [join(process.cwd(), 'scripts', 'monorepo.mjs'), 'port', 'continue', `--target=${target}`, '--stage', '--json'],
850
+ { cwd: process.cwd(), env: gitEnv() }
851
+ );
852
+ const parsed = JSON.parse(out.trim());
853
+ assert.equal(parsed.ok, true);
854
+ assert.equal(parsed.inProgress, false);
855
+
856
+ const content = (await readFile(join(target, 'cli', 'hello.txt'), 'utf-8')).toString();
857
+ assert.equal(content, 'value=source\n');
858
+ });
859
+
724
860
  test('monorepo port guide refuses to run in non-tty mode', async (t) => {
725
861
  const root = await withTempRoot(t);
726
862
  const target = join(root, 'target-mono');
@@ -73,7 +73,11 @@ corepack prepare pnpm@latest --activate >/dev/null 2>&1 || true
73
73
 
74
74
  echo "[provision] configuring happy-stacks VM defaults (ports)..."
75
75
  # When port-forwarding a VM to the macOS host, it's convenient to avoid using the host's default ports (3005/8081).
76
- # Persist these as happy-stacks home defaults so `npx happy-stacks ...` picks them up automatically.
76
+ # Persist these as happy-stacks *home* defaults so non-sandbox `happys ...` commands pick them up automatically.
77
+ #
78
+ # NOTE: `npx happy-stacks review-pr ...` runs in a fully isolated sandbox (separate HOME), so it will NOT read
79
+ # this file by default. For review-pr in a VM, pass `--vm-ports` (or explicit `--stack-port-start=...`) to
80
+ # force the port ranges inside the sandbox.
77
81
  HS_HOME="${HOME}/.happy-stacks"
78
82
  mkdir -p "$HS_HOME"
79
83
  ENV_LOCAL="${HS_HOME}/env.local"
@@ -48,15 +48,23 @@ fi
48
48
 
49
49
  VM_NAME="${1:-happy-test}"
50
50
  TEMPLATE="${LIMA_TEMPLATE:-ubuntu-24.04}"
51
+ LIMA_MEMORY="${LIMA_MEMORY:-8GiB}"
52
+ TEMPLATE_LOCATOR="${TEMPLATE}"
53
+ if [[ "${TEMPLATE_LOCATOR}" == template://* ]]; then
54
+ TEMPLATE_LOCATOR="template:${TEMPLATE_LOCATOR#template://}"
55
+ elif [[ "${TEMPLATE_LOCATOR}" != template:* ]]; then
56
+ TEMPLATE_LOCATOR="template:${TEMPLATE_LOCATOR}"
57
+ fi
51
58
  LIMA_DIR="${HOME}/.lima/${VM_NAME}"
52
59
  LIMA_YAML="${LIMA_DIR}/lima.yaml"
53
60
 
54
61
  echo "[lima] vm: ${VM_NAME}"
55
62
  echo "[lima] template: ${TEMPLATE}"
63
+ echo "[lima] memory: ${LIMA_MEMORY} (override with LIMA_MEMORY=...)"
56
64
 
57
65
  if [[ ! -f "${LIMA_YAML}" ]]; then
58
66
  echo "[lima] creating VM..."
59
- limactl create --name "${VM_NAME}" --tty=false "template://${TEMPLATE}"
67
+ limactl create --name "${VM_NAME}" --tty=false "${TEMPLATE_LOCATOR}"
60
68
  fi
61
69
 
62
70
  if [[ ! -f "${LIMA_YAML}" ]]; then
@@ -70,14 +78,17 @@ limactl stop "${VM_NAME}" >/dev/null 2>&1 || true
70
78
  echo "[lima] configuring port forwarding (localhost)..."
71
79
  cp -a "${LIMA_YAML}" "${LIMA_YAML}.bak.$(date +%Y%m%d-%H%M%S)"
72
80
 
73
- python3 - <<'PY'
81
+ VM_NAME="${VM_NAME}" LIMA_YAML="${LIMA_YAML}" LIMA_MEMORY="${LIMA_MEMORY}" python3 - <<'PY'
74
82
  import os, re
75
83
  from pathlib import Path
76
84
 
77
85
  vm_name = os.environ["VM_NAME"]
78
86
  path = Path(os.environ["LIMA_YAML"])
87
+ memory = os.environ.get("LIMA_MEMORY", "8GiB")
79
88
  text = path.read_text(encoding="utf-8")
80
89
 
90
+ MEM_MARK_BEGIN = "# --- happy-stacks vm sizing (added by happy-local) ---"
91
+ MEM_MARK_END = "# --- /happy-stacks vm sizing ---"
81
92
  MARK_BEGIN = "# --- happy-stacks port forwards (added by happy-local) ---"
82
93
  MARK_END = "# --- /happy-stacks port forwards ---"
83
94
 
@@ -86,6 +97,12 @@ entries = [
86
97
  " - guestPortRange: [18000, 19099]\n hostPortRange: [18000, 19099]\n",
87
98
  ]
88
99
 
100
+ mem_block = (
101
+ f"\n{MEM_MARK_BEGIN}\n"
102
+ f'memory: "{memory}"\n'
103
+ f"{MEM_MARK_END}\n"
104
+ )
105
+
89
106
  block_as_section = (
90
107
  f"\n{MARK_BEGIN}\n"
91
108
  "portForwards:\n"
@@ -99,6 +116,20 @@ block_as_list_items = (
99
116
  f" # --- /happy-stacks port forwards ---\n"
100
117
  )
101
118
 
119
+ if MEM_MARK_BEGIN in text and MEM_MARK_END in text:
120
+ text = re.sub(
121
+ re.escape(MEM_MARK_BEGIN) + r"[\\s\\S]*?" + re.escape(MEM_MARK_END) + r"\\n?",
122
+ mem_block.strip("\n") + "\n",
123
+ text,
124
+ flags=re.MULTILINE,
125
+ )
126
+ else:
127
+ m = re.search(r"^memory:\\s*.*$", text, flags=re.MULTILINE)
128
+ if m:
129
+ text = re.sub(r"^memory:\\s*.*$", f'memory: "{memory}"', text, flags=re.MULTILINE)
130
+ else:
131
+ text = text.rstrip() + mem_block
132
+
102
133
  if MARK_BEGIN in text and MARK_END in text:
103
134
  text = re.sub(
104
135
  re.escape(MARK_BEGIN) + r"[\\s\\S]*?" + re.escape(MARK_END) + r"\\n?",
@@ -138,5 +169,6 @@ Then:
138
169
 
139
170
  Tip:
140
171
  Open the printed URLs on your macOS host via http://localhost:<port> or http://*.localhost:<port>.
172
+ For `npx happy-stacks review-pr ...` inside the VM, pass `--vm-ports` so stack ports land in the forwarded ranges.
141
173
  EOF
142
174