gwqadd 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -135,6 +135,11 @@ question. At that point the sentence, the repository name, its branch names and
135
135
  your modified file *paths* (never contents) go to that CLI. Nothing is created
136
136
  until you confirm.
137
137
 
138
+ The CLI is run in an **empty temporary directory**, not in your repository.
139
+ These tools are agents: given a repo they will read `CLAUDE.md`, your source and
140
+ your git log, and then name the branch after what they found instead of what you
141
+ asked for. Everything they should know is already in the prompt.
142
+
138
143
  None of this happens when you pass a branch name on the command line, or when
139
144
  there is no terminal. Scripts and agents keep the plain, silent contract.
140
145
 
package/bin/gwqadd.mjs CHANGED
@@ -2,7 +2,12 @@
2
2
  import { spawnSync, spawn } from 'node:child_process';
3
3
  import { parseArgs } from 'node:util';
4
4
  import { Buffer } from 'node:buffer';
5
- import { readFileSync, existsSync, readdirSync, renameSync, realpathSync } from 'node:fs';
5
+ import {
6
+ readFileSync, existsSync, readdirSync, renameSync, realpathSync,
7
+ mkdtempSync, rmSync,
8
+ } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join as joinPath } from 'node:path';
6
11
  import { fileURLToPath } from 'node:url';
7
12
  import { createInterface } from 'node:readline/promises';
8
13
 
@@ -627,6 +632,7 @@ function namingPrompt(ctx, description, rejected) {
627
632
  ? `Files already modified in the working tree, which is what the work touches:\n${ctx.dirty.map((f) => ` ${f}`).join('\n')}`
628
633
  : '',
629
634
  'Include the prefix and a slash. After it use lowercase ASCII words joined by hyphens. Under 40 characters. Be specific about this change, not generic.',
635
+ 'The description below is the only thing the branch is about. Do not name it after anything else you can see.',
630
636
  rejected.length
631
637
  ? `These were rejected — do not propose them or close variants:\n${rejected.map((r) => ` ${r}`).join('\n')}`
632
638
  : '',
@@ -662,15 +668,42 @@ function detectAi() {
662
668
  return AI_CLIS.find((c) => commandExists(c.bin)) ?? null;
663
669
  }
664
670
 
665
- // spawnSync would freeze the terminal for the 6-8 seconds these CLIs take to
666
- // boot, with no sign of life. Async plus an elapsed counter is the difference
671
+ // These CLIs are agents, not text transformers: run one inside a repository and
672
+ // it reads CLAUDE.md, the source and the git history, then names the branch
673
+ // after what it found instead of what the user asked for. Measured in this very
674
+ // repository — description "uiのバグの修正", three runs each:
675
+ //
676
+ // cwd = the repo feat/ui-bug-fix, fix/ui-display-bug, feat/ui-bug-fix
677
+ // cwd = empty dir fix/ui-display-bug, fix/ui-display-bug, fix/ui-display-bug
678
+ //
679
+ // In-repo it both wavered and picked `feat/` for a bug fix twice. It once
680
+ // answered `feat/naming-prompt-repo-context`, which is a phrase straight out of
681
+ // this repo's CLAUDE.md. Everything the model legitimately needs is already in
682
+ // the prompt, so it runs in an empty directory and is given nothing else.
683
+ //
684
+ // spawnSync would also freeze the terminal for the 6-8 seconds these CLIs take
685
+ // to boot, with no sign of life; async plus an elapsed counter is the difference
667
686
  // between "working" and "hung".
668
687
  function runAi(ai, prompt) {
669
688
  return new Promise((resolve) => {
689
+ let sandbox = '';
690
+ try {
691
+ sandbox = mkdtempSync(joinPath(tmpdir(), 'gwqadd-ai-'));
692
+ } catch { /* fall back to inheriting cwd rather than not answering at all */ }
693
+ const cleanup = () => {
694
+ if (!sandbox) return;
695
+ try { rmSync(sandbox, { recursive: true, force: true }); } catch { /* ignore */ }
696
+ sandbox = '';
697
+ };
698
+
670
699
  let child;
671
700
  try {
672
- child = spawn(ai.bin, [...ai.args, prompt], { stdio: ['ignore', 'pipe', 'pipe'] });
701
+ child = spawn(ai.bin, [...ai.args, prompt], {
702
+ stdio: ['ignore', 'pipe', 'pipe'],
703
+ ...(sandbox ? { cwd: sandbox } : {}),
704
+ });
673
705
  } catch (err) {
706
+ cleanup();
674
707
  return resolve({ ok: false, out: '', err: String(err?.message ?? err) });
675
708
  }
676
709
  let out = '';
@@ -680,10 +713,12 @@ function runAi(ai, prompt) {
680
713
  const kill = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* gone */ } }, 60_000);
681
714
  child.on('error', (e) => {
682
715
  clearTimeout(kill);
716
+ cleanup();
683
717
  resolve({ ok: false, out: '', err: String(e?.message ?? e) });
684
718
  });
685
719
  child.on('close', (code) => {
686
720
  clearTimeout(kill);
721
+ cleanup();
687
722
  resolve({ ok: code === 0, out, err });
688
723
  });
689
724
  });
@@ -906,9 +941,36 @@ function copyToClipboard(text) {
906
941
 
907
942
  // ── worktree creation ────────────────────────────────────────────────────────
908
943
 
909
- // gwq reports the git command it ran, so a collision's destination can be read
910
- // back out of its error text.
911
- const COLLISION = /git worktree add (?:-b [^ ]* )?(\/[^ :]*)/;
944
+ // A collision's destination has to be recovered from gwq's error text. Two
945
+ // sources, in order of reliability:
946
+ //
947
+ // fatal: '<path>' already exists <- git, quoted, unambiguous
948
+ // ...: git worktree add [-b <branch>] <path>: ...
949
+ //
950
+ // The quoted form is preferred because the command echo is not parseable in
951
+ // general: `-b` swaps the argument order (`add -b <branch> <path>` versus
952
+ // `add <path> <branch>`), and a path containing a space silently truncated the
953
+ // old pattern — a gwq basedir under a directory with a space made `-f` do
954
+ // nothing at all, without saying so.
955
+ const COLLISION_QUOTED = /fatal: '([^']+)' already exists/;
956
+
957
+ // The command echo needs to know which form was used, because `-b` swaps the
958
+ // argument order and both a path and a branch can follow `add`:
959
+ //
960
+ // gwq add -b <branch> -> git worktree add -b <branch> <path>: …
961
+ // gwq add <branch> -> git worktree add <path> <branch>: …
962
+ //
963
+ // Guessing cost real time once already: a pattern that stopped at the first
964
+ // space read the path-first form correctly by accident, and a pattern that ran
965
+ // to the colon swallowed the branch with it.
966
+ const collisionFromCmd = (out, withB) => (withB
967
+ ? out.match(/git worktree add -b \S+ (.+?): /)
968
+ : out.match(/git worktree add (.+?) \S+: /))?.[1];
969
+
970
+ function collisionPath(out, withB) {
971
+ const quoted = out.match(COLLISION_QUOTED)?.[1];
972
+ return (quoted ?? collisionFromCmd(out, withB) ?? '').trim();
973
+ }
912
974
 
913
975
  function timestamp() {
914
976
  const d = new Date();
@@ -1019,7 +1081,7 @@ async function main() {
1019
1081
  };
1020
1082
 
1021
1083
  if (status !== 0) {
1022
- const collide = out.match(COLLISION)?.[1] ?? '';
1084
+ const collide = collisionPath(out, addArgs[0] === '-b');
1023
1085
  // gwq's own -f does not reach `git worktree add` (verified against v0.1.1),
1024
1086
  // so a collision has to be cleared here or not at all.
1025
1087
  if (collide && existsSync(collide) && force) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gwqadd",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Create a branch and its gwq worktree in the repository you are in, and cd there.",
5
5
  "type": "module",
6
6
  "bin": {