@marcomigozzi/trailhead 0.14.0 → 0.16.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 (156) hide show
  1. package/README.it.md +158 -34
  2. package/README.md +147 -33
  3. package/bin/auto-intro-docs.test.js +72 -0
  4. package/bin/auto-merge-docs.test.js +33 -0
  5. package/bin/auto-merge-ticket-docs.test.js +32 -0
  6. package/bin/auto-review-rounds-docs.test.js +45 -0
  7. package/bin/auto-run-docs.test.js +60 -0
  8. package/bin/auto-scope-docs.test.js +33 -0
  9. package/bin/auto-slots-docs.test.js +45 -0
  10. package/bin/auto-uat-docs.test.js +47 -0
  11. package/bin/base-preflight-docs.test.js +65 -0
  12. package/bin/execute-split-docs.test.js +146 -0
  13. package/bin/grilling-holistic-review-docs.test.js +90 -0
  14. package/bin/isolation-answer-docs.test.js +41 -0
  15. package/bin/isolation-copy-docs.test.js +28 -0
  16. package/bin/lib/codex-projection.js +6 -6
  17. package/bin/lib/codex-projection.test.js +43 -2
  18. package/bin/needs-you-docs.test.js +41 -0
  19. package/bin/pick-stress-test-docs.test.js +105 -0
  20. package/bin/report-status-docs.test.js +195 -0
  21. package/bin/resolve-helpers-docs.test.js +36 -0
  22. package/bin/scope-confirm-skip-docs.test.js +70 -0
  23. package/bin/trailhead.js +6 -6
  24. package/bin/trailhead.test.js +355 -16
  25. package/bin/uat-setup-docs.test.js +65 -0
  26. package/package.json +1 -1
  27. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  28. package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
  29. package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
  30. package/plugins/trailhead/agents/trailhead-debug.md +20 -1
  31. package/plugins/trailhead/agents/trailhead-executor.md +29 -2
  32. package/plugins/trailhead/agents/trailhead-fix.md +29 -2
  33. package/plugins/trailhead/agents/trailhead-plan.md +20 -1
  34. package/plugins/trailhead/agents/trailhead-research.md +22 -3
  35. package/plugins/trailhead/agents/trailhead-verify.md +15 -2
  36. package/plugins/trailhead/commands/auto.md +2 -2
  37. package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
  38. package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
  39. package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
  40. package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
  41. package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
  42. package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
  43. package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
  44. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
  45. package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
  46. package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
  47. package/plugins/trailhead/hooks/lib/dashboard.test.js +472 -0
  48. package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
  49. package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
  50. package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
  51. package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
  52. package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
  53. package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
  54. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
  55. package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
  56. package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
  57. package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
  58. package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
  59. package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
  60. package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
  61. package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
  62. package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
  63. package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
  64. package/plugins/trailhead/hooks/mod/README.md +209 -36
  65. package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
  66. package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
  67. package/plugins/trailhead/hooks/mod/map-pane-view.ts +304 -26
  68. package/plugins/trailhead/hooks/mod/map-pane.tsx +763 -160
  69. package/plugins/trailhead/hooks/mod/register.tsx +312 -40
  70. package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
  71. package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
  72. package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
  73. package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
  74. package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
  75. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
  76. package/plugins/trailhead/hooks/mod/work-state-view.ts +38 -5
  77. package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
  78. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  79. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
  80. package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
  81. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
  82. package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
  83. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
  84. package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
  85. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
  86. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
  87. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
  88. package/plugins/trailhead/skills/_shared/choices.md +11 -4
  89. package/plugins/trailhead/skills/_shared/configuration-reference.md +17 -5
  90. package/plugins/trailhead/skills/_shared/load-first.md +3 -3
  91. package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
  92. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  93. package/plugins/trailhead/skills/_shared/substrate-commands.md +47 -14
  94. package/plugins/trailhead/skills/_shared/substrate.md +9 -4
  95. package/plugins/trailhead/skills/_shared/teamwork.md +17 -8
  96. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +18 -10
  97. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  98. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  99. package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
  100. package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
  101. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
  102. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
  103. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  104. package/plugins/trailhead/skills/_shared/techniques.md +12 -2
  105. package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
  106. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
  107. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +3 -3
  108. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
  109. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
  110. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
  111. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
  112. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  113. package/plugins/trailhead/skills/trailhead-view/SKILL.md +9 -6
  114. package/plugins/trailhead/skills/trailhead-work/SKILL.md +31 -18
  115. package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
  116. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
  117. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +29 -13
  118. package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
  119. package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
  120. package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
  121. package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
  122. package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
  123. package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
  124. package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
  125. package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
  126. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
  127. package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
  128. package/plugins/trailhead/templates/trailhead-dashboard.js +360 -0
  129. package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
  130. package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
  131. package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
  132. package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
  133. package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
  134. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
  135. package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
  136. package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
  137. package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
  138. package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
  139. package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
  140. package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
  141. package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
  142. package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
  143. package/plugins/trailhead/tests/map-pane-ui.test.ts +2519 -382
  144. package/plugins/trailhead/tests/map-pane-view.test.ts +600 -18
  145. package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
  146. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
  147. package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
  148. package/plugins/trailhead/tests/session-registry.test.ts +50 -0
  149. package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
  150. package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
  151. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
  152. package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
  153. package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
  154. package/plugins/trailhead/tests/work-state-view.test.ts +44 -1
  155. package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
  156. package/plugins/trailhead/types/index.d.ts +62 -3
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ // Tests for templates/trailhead-auto-slot.js. Run: node auto-slot.test.js
3
+ // No framework: plain asserts, same style as auto-queue.test.js.
4
+ //
5
+ // Layers covered:
6
+ // 1. main() open / list against REAL temporary git repos (the default git runner).
7
+ // 2. Usage errors (exit 2).
8
+ // 3. End-to-end: the script itself through spawnSync.
9
+ const assert = require('assert');
10
+ const { spawnSync } = require('child_process');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+
15
+ const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-auto-slot.js');
16
+ const { main } = require(scriptPath);
17
+
18
+ let passed = 0;
19
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
20
+
21
+ const git = (cwd, ...args) => {
22
+ const r = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8' });
23
+ if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr}`);
24
+ return r.stdout.trim();
25
+ };
26
+
27
+ // A parent dir holding <parent>/repo with two commits. Returns { parent, repo, first, second }.
28
+ function makeRepo() {
29
+ const parent = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'auto-slot-')));
30
+ const repo = path.join(parent, 'repo');
31
+ fs.mkdirSync(repo);
32
+ git(repo, 'init', '-q', '-b', 'main');
33
+ git(repo, 'config', 'user.name', 'T');
34
+ git(repo, 'config', 'user.email', 't@example.com');
35
+ git(repo, 'config', 'commit.gpgsign', 'false');
36
+ fs.writeFileSync(path.join(repo, 'a.txt'), 'one\n');
37
+ git(repo, 'add', 'a.txt');
38
+ git(repo, 'commit', '-q', '-m', 'first');
39
+ const first = git(repo, 'rev-parse', 'HEAD');
40
+ fs.writeFileSync(path.join(repo, 'a.txt'), 'two\n');
41
+ git(repo, 'commit', '-q', '-am', 'second');
42
+ const second = git(repo, 'rev-parse', 'HEAD');
43
+ return { parent, repo, first, second };
44
+ }
45
+
46
+ function run(argv, deps = {}) {
47
+ let out = '';
48
+ let errOut = '';
49
+ const code = main(argv, { stdout: { write: (s) => { out += s; } }, stderr: { write: (s) => { errOut += s; } }, ...deps });
50
+ let json = null;
51
+ try { json = JSON.parse(out.trim()); } catch { /* not json */ }
52
+ return { code, out, json, errOut };
53
+ }
54
+
55
+ const open = (repo, n, base, deps) => run(['open', '--repo', repo, '--ticket', String(n), '--base', base], deps);
56
+ const list = (repo, deps) => run(['list', '--repo', repo], deps);
57
+
58
+ // --- open: created -----------------------------------------------------------
59
+ {
60
+ const { parent, repo, first, second } = makeRepo();
61
+ const r = open(repo, 7, first);
62
+ const wt = path.join(parent, 'repo-t7');
63
+ ok('open: created exits 0', r.code === 0);
64
+ ok('open: created status, ticket, branch, path', r.json.status === 'created' && r.json.ticket === 7 && r.json.branch === 'trailhead/t7' && r.json.path === wt);
65
+ ok('open: the worktree directory exists', fs.existsSync(path.join(wt, 'a.txt')));
66
+ ok('open: branch points exactly at --base', git(repo, 'rev-parse', 'refs/heads/trailhead/t7') === first);
67
+ ok('open: branch is not the repo HEAD', git(repo, 'rev-parse', 'refs/heads/trailhead/t7') !== second && git(repo, 'rev-parse', 'HEAD') === second);
68
+ ok('open: worktree checked out on the branch', git(wt, 'rev-parse', '--abbrev-ref', 'HEAD') === 'trailhead/t7');
69
+
70
+ // reentered: same call again, nothing new created
71
+ const again = open(repo, 7, second);
72
+ ok('open: reentered when the branch is already checked out there', again.code === 0 && again.json.status === 'reentered' && again.json.path === wt);
73
+ ok('open: reentered does not move the branch', git(repo, 'rev-parse', 'refs/heads/trailhead/t7') === first);
74
+
75
+ // reattached: worktree removed, branch kept
76
+ git(repo, 'worktree', 'remove', '--force', wt);
77
+ const re = open(repo, 7, second);
78
+ ok('open: reattached when the branch exists without a worktree', re.code === 0 && re.json.status === 'reattached' && re.json.path === wt);
79
+ ok('open: reattached keeps the branch commit', git(repo, 'rev-parse', 'refs/heads/trailhead/t7') === first && git(wt, 'rev-parse', 'HEAD') === first);
80
+ fs.rmSync(parent, { recursive: true, force: true });
81
+ }
82
+
83
+ // --- open: branch checked out at another path -> reentered with that path ---
84
+ {
85
+ const { parent, repo, first } = makeRepo();
86
+ const other = path.join(parent, 'elsewhere');
87
+ git(repo, 'worktree', 'add', '-q', '-b', 'trailhead/t9', other, first);
88
+ const r = open(repo, 9, first);
89
+ ok('open: a hand-made worktree elsewhere is reentered with its path', r.code === 0 && r.json.status === 'reentered' && r.json.path === other);
90
+ ok('open: nothing created at the sibling path', !fs.existsSync(path.join(parent, 'repo-t9')));
91
+ fs.rmSync(parent, { recursive: true, force: true });
92
+ }
93
+
94
+ // --- open: path collision ----------------------------------------------------
95
+ {
96
+ const { parent, repo, first } = makeRepo();
97
+ const target = path.join(parent, 'repo-t3');
98
+ fs.mkdirSync(target);
99
+ fs.writeFileSync(path.join(target, 'keep.txt'), 'mine\n');
100
+ const r = open(repo, 3, first);
101
+ ok('open: existing path is an error', r.code === 1 && r.json.status === 'error' && /exist/.test(r.json.reason));
102
+ ok('open: existing path is never overwritten', fs.readFileSync(path.join(target, 'keep.txt'), 'utf8') === 'mine\n');
103
+ ok('open: no branch created on a path collision', spawnSync('git', ['-C', repo, 'rev-parse', '--verify', '--quiet', 'refs/heads/trailhead/t3']).status !== 0);
104
+
105
+ // reattach into an occupied path is an error too
106
+ git(repo, 'branch', 'trailhead/t4', first);
107
+ fs.mkdirSync(path.join(parent, 'repo-t4'));
108
+ const r4 = open(repo, 4, first);
109
+ ok('open: reattach onto an existing path is an error', r4.code === 1 && r4.json.status === 'error');
110
+ fs.rmSync(parent, { recursive: true, force: true });
111
+ }
112
+
113
+ // --- open: invalid base ------------------------------------------------------
114
+ {
115
+ const { parent, repo } = makeRepo();
116
+ const r = open(repo, 5, 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef');
117
+ ok('open: unresolvable base is an error', r.code === 1 && r.json.status === 'error' && /base/.test(r.json.reason));
118
+ ok('open: nothing created for an invalid base', !fs.existsSync(path.join(parent, 'repo-t5')));
119
+ fs.rmSync(parent, { recursive: true, force: true });
120
+ }
121
+
122
+ // --- open: clone-required on a superrepo -------------------------------------
123
+ {
124
+ const { parent, repo, first } = makeRepo();
125
+ fs.writeFileSync(path.join(repo, '.gitmodules'), '[submodule "lib"]\n\tpath = lib\n\turl = https://example.com/lib.git\n');
126
+ const r = open(repo, 8, first);
127
+ ok('open: a declared submodule needs a clone', r.code === 0 && r.json.status === 'clone-required' && r.json.path === path.join(parent, 'repo-t8') && r.json.branch === 'trailhead/t8');
128
+ ok('open: clone-required creates no worktree', !fs.existsSync(path.join(parent, 'repo-t8')));
129
+ fs.writeFileSync(path.join(repo, '.gitmodules'), '# nothing declared\n');
130
+ const none = open(repo, 8, first);
131
+ ok('open: a .gitmodules without a submodule section does not force a clone', none.json.status === 'created');
132
+ fs.rmSync(parent, { recursive: true, force: true });
133
+ }
134
+
135
+ // --- list --------------------------------------------------------------------
136
+ {
137
+ const { parent, repo, first, second } = makeRepo();
138
+ ok('list: a repo with no slot is empty', list(repo).json.slots.length === 0);
139
+
140
+ open(repo, 12, first);
141
+ open(repo, 4, second);
142
+ // worktree on a non-slot branch is not a slot
143
+ git(repo, 'worktree', 'add', '-q', '-b', 'feature/x', path.join(parent, 'feature-x'), first);
144
+
145
+ // a sibling clone on trailhead/t21
146
+ const clone = path.join(parent, 'repo-t21');
147
+ git(parent, 'clone', '-q', repo, clone);
148
+ git(clone, 'config', 'user.name', 'T');
149
+ git(clone, 'config', 'user.email', 't@example.com');
150
+ git(clone, 'checkout', '-q', '-b', 'trailhead/t21', first);
151
+ // a sibling clone on the wrong branch is ignored
152
+ const wrong = path.join(parent, 'repo-t22');
153
+ git(parent, 'clone', '-q', repo, wrong);
154
+ git(wrong, 'checkout', '-q', '-b', 'trailhead/t99');
155
+ // a sibling dir that is not a git checkout is ignored
156
+ fs.mkdirSync(path.join(parent, 'repo-t30'));
157
+ // a sibling dir inside another checkout (no own git dir) is ignored
158
+ fs.mkdirSync(path.join(repo, 'repo-t31'));
159
+
160
+ const wt12 = path.join(parent, 'repo-t12');
161
+ const wt4 = path.join(parent, 'repo-t4');
162
+ // work state: present on t12, unparseable on t4, absent on the clone
163
+ fs.mkdirSync(path.join(wt12, '.trailhead'), { recursive: true });
164
+ fs.writeFileSync(path.join(wt12, '.trailhead', 'work-state.json'), JSON.stringify({ ticket: 12, auto: true }));
165
+ fs.mkdirSync(path.join(wt4, '.trailhead'), { recursive: true });
166
+ fs.writeFileSync(path.join(wt4, '.trailhead', 'work-state.json'), '{not json');
167
+ // dirty: t12 has an untracked file (.trailhead is untracked too, so both worktrees are dirty); clone is clean
168
+ fs.writeFileSync(path.join(wt12, 'new.txt'), 'x\n');
169
+
170
+ const r = list(repo);
171
+ ok('list: exits 0 with status ok', r.code === 0 && r.json.status === 'ok');
172
+ ok('list: slots sorted by ticket', JSON.stringify(r.json.slots.map((s) => s.ticket)) === JSON.stringify([4, 12, 21]));
173
+ const by = Object.fromEntries(r.json.slots.map((s) => [s.ticket, s]));
174
+ ok('list: worktree kind, branch and path', by[12].kind === 'worktree' && by[12].branch === 'trailhead/t12' && by[12].path === wt12);
175
+ ok('list: sibling clone is listed as clone', by[21].kind === 'clone' && by[21].branch === 'trailhead/t21' && by[21].path === clone);
176
+ ok('list: non-slot worktree, wrong-branch clone, non-git dir and nested dir are ignored', !r.json.slots.some((s) => [99, 30, 31].includes(s.ticket)) && r.json.slots.length === 3);
177
+ ok('list: work state present is parsed', by[12].workState && by[12].workState.ticket === 12 && by[12].workState.auto === true);
178
+ ok('list: work state absent is null without a warning', by[21].workState === null && !r.json.warnings.some((w) => w.includes('repo-t21')));
179
+ ok('list: unparseable work state is null plus a warning', by[4].workState === null && r.json.warnings.length === 1 && r.json.warnings[0].includes('repo-t4'));
180
+ ok('list: dirty detected', by[12].dirty === true);
181
+ ok('list: a clean checkout is not dirty', by[21].dirty === false);
182
+ fs.rmSync(parent, { recursive: true, force: true });
183
+ }
184
+
185
+ // --- W1: stale worktree (directory deleted by hand) --------------------------
186
+ {
187
+ const { parent, repo, first } = makeRepo();
188
+ open(repo, 6, first);
189
+ open(repo, 7, first);
190
+ const wt = path.join(parent, 'repo-t6');
191
+ fs.rmSync(wt, { recursive: true, force: true });
192
+ const r = open(repo, 6, first);
193
+ ok('open: a stale worktree is an error naming it and git worktree prune', r.code === 1 && r.json.status === 'error' && r.json.reason.includes(wt) && /stale/.test(r.json.reason) && r.json.reason.includes('git worktree prune'));
194
+ ok('open: a stale worktree is not pruned automatically', git(repo, 'worktree', 'list', '--porcelain').includes(wt));
195
+ const l = list(repo);
196
+ ok('list: a stale worktree is kept out of slots', l.code === 0 && JSON.stringify(l.json.slots.map((x) => x.ticket)) === JSON.stringify([7]));
197
+ ok('list: a stale worktree is a warning naming it and suggesting prune', l.json.warnings.length === 1 && l.json.warnings[0].includes(wt) && /stale/.test(l.json.warnings[0]) && l.json.warnings[0].includes('git worktree prune'));
198
+ fs.rmSync(parent, { recursive: true, force: true });
199
+ }
200
+
201
+ // --- W2: superrepo with an existing slot path --------------------------------
202
+ {
203
+ const { parent, repo, first } = makeRepo();
204
+ fs.writeFileSync(path.join(repo, '.gitmodules'), '[submodule "lib"]\n\tpath = lib\n\turl = https://example.com/lib.git\n');
205
+ const target = path.join(parent, 'repo-t8');
206
+ const missing = open(repo, 8, first);
207
+ ok('open: missing path on a superrepo is clone-required with kind clone', missing.json.status === 'clone-required' && missing.json.kind === 'clone');
208
+
209
+ fs.mkdirSync(target);
210
+ fs.writeFileSync(path.join(target, 'keep.txt'), 'mine\n');
211
+ const plain = open(repo, 8, first);
212
+ ok('open: a plain existing dir on a superrepo is an error, never clone-required', plain.code === 1 && plain.json.status === 'error');
213
+ ok('open: the existing dir is untouched', fs.readFileSync(path.join(target, 'keep.txt'), 'utf8') === 'mine\n');
214
+ fs.rmSync(target, { recursive: true, force: true });
215
+
216
+ git(parent, 'clone', '-q', repo, target);
217
+ git(target, 'checkout', '-q', '-b', 'trailhead/t8', first);
218
+ const clone = open(repo, 8, first);
219
+ ok('open: an existing clone on the slot branch is reentered with kind clone', clone.code === 0 && clone.json.status === 'reentered' && clone.json.kind === 'clone' && clone.json.path === target && clone.json.branch === 'trailhead/t8');
220
+
221
+ git(target, 'checkout', '-q', '-b', 'other');
222
+ const wrong = open(repo, 8, first);
223
+ ok('open: an existing clone on another branch is an error', wrong.code === 1 && wrong.json.status === 'error');
224
+ fs.rmSync(parent, { recursive: true, force: true });
225
+ }
226
+ {
227
+ const { parent, repo, first } = makeRepo();
228
+ ok('open: worktree statuses carry kind worktree', open(repo, 1, first).json.kind === 'worktree' && open(repo, 1, first).json.kind === 'worktree');
229
+ git(repo, 'worktree', 'remove', '--force', path.join(parent, 'repo-t1'));
230
+ const rea = open(repo, 1, first);
231
+ ok('open: reattached carries kind worktree', rea.json.status === 'reattached' && rea.json.kind === 'worktree');
232
+ fs.rmSync(parent, { recursive: true, force: true });
233
+ }
234
+
235
+ // --- list: a git failure on the main repo ------------------------------------
236
+ {
237
+ const notRepo = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'auto-slot-nr-')));
238
+ const r = list(notRepo);
239
+ ok('list: git failing on the main repo is an error', r.code === 1 && r.json.status === 'error');
240
+ fs.rmSync(notRepo, { recursive: true, force: true });
241
+ }
242
+
243
+ // --- injectable git runner ---------------------------------------------------
244
+ {
245
+ const calls = [];
246
+ const fake = (args) => { calls.push(args.join(' ')); return { status: 1, stdout: '', stderr: 'boom' }; };
247
+ const r = list('/nowhere/repo', { git: fake });
248
+ ok('seam: injected git runner is used and its failure is an error', r.code === 1 && r.json.status === 'error' && calls.length >= 1 && calls[0].startsWith('-C /nowhere/repo'));
249
+ }
250
+
251
+ // --- usage errors ------------------------------------------------------------
252
+ {
253
+ const u = (argv) => run(argv);
254
+ ok('usage: no command exits 2', u([]).code === 2);
255
+ ok('usage: unknown command exits 2', u(['remove', '--repo', '/x']).code === 2);
256
+ ok('usage: open without --ticket exits 2', u(['open', '--repo', '/x', '--base', 'abc']).code === 2);
257
+ ok('usage: open without --base exits 2', u(['open', '--repo', '/x', '--ticket', '3']).code === 2);
258
+ ok('usage: open without --repo exits 2', u(['open', '--ticket', '3', '--base', 'abc']).code === 2);
259
+ ok('usage: bad --ticket exits 2', u(['open', '--repo', '/x', '--ticket', 'x', '--base', 'abc']).code === 2);
260
+ ok('usage: list without --repo exits 2', u(['list']).code === 2);
261
+ ok('usage: unknown flag exits 2', u(['list', '--repo', '/x', '--bogus', '1']).code === 2);
262
+ ok('usage: errors go to stderr, stdout stays empty', u(['open']).errOut.startsWith('usage:') && u(['open']).out === '');
263
+ }
264
+
265
+ // --- end-to-end --------------------------------------------------------------
266
+ {
267
+ const { parent, repo, first } = makeRepo();
268
+ const res = spawnSync('node', [scriptPath, 'open', '--repo', repo, '--ticket', '15', '--base', first], { encoding: 'utf8' });
269
+ const j = JSON.parse(res.stdout.trim());
270
+ ok('e2e: open prints one JSON line and exits 0', res.status === 0 && j.status === 'created' && j.path === path.join(parent, 'repo-t15'));
271
+ const ls = spawnSync('node', [scriptPath, 'list', '--repo', repo], { encoding: 'utf8' });
272
+ const lj = JSON.parse(ls.stdout.trim());
273
+ ok('e2e: list finds the new slot', ls.status === 0 && lj.slots.length === 1 && lj.slots[0].ticket === 15);
274
+ ok('e2e: usage error exits 2', spawnSync('node', [scriptPath, '--bogus'], { encoding: 'utf8' }).status === 2);
275
+ fs.rmSync(parent, { recursive: true, force: true });
276
+ }
277
+
278
+ console.log(`auto-slot.test.js: ${passed} checks passed`);