@anhvupt/tito 0.3.1 → 0.3.3

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.
@@ -0,0 +1,295 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { ConfigValidationError, parseConfig } from "./config.js";
5
+ import { BRANCH_BASES } from "./git-flow.js";
6
+ export class WorktreeError extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = "WorktreeError";
11
+ this.code = code;
12
+ }
13
+ }
14
+ const BASE_BRANCHES = new Set(BRANCH_BASES);
15
+ function errno(error) {
16
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") {
17
+ return error.code;
18
+ }
19
+ return null;
20
+ }
21
+ function isDirectory(path) {
22
+ try {
23
+ return statSync(path).isDirectory();
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ function isMainCheckout(path) {
30
+ try {
31
+ return statSync(join(path, ".git")).isDirectory();
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function spawnGit(cwd, args) {
38
+ const result = spawnSync("git", [...args], { cwd, encoding: "utf8" });
39
+ if (result.error)
40
+ throw new WorktreeError("git", result.error.message);
41
+ return {
42
+ status: result.status ?? 1,
43
+ stdout: result.stdout ?? "",
44
+ stderr: result.stderr ?? "",
45
+ };
46
+ }
47
+ function gitOk(cwd, args) {
48
+ const result = spawnGit(cwd, args);
49
+ if (result.status !== 0) {
50
+ const detail = (result.stderr || result.stdout || "git command failed").trim();
51
+ throw new WorktreeError("git", detail);
52
+ }
53
+ return result.stdout;
54
+ }
55
+ function parseCount(stdout) {
56
+ const count = Number(stdout.trim());
57
+ if (!Number.isInteger(count) || count < 0) {
58
+ throw new WorktreeError("git", `Unexpected rev-list count: ${stdout.trim()}`);
59
+ }
60
+ return count;
61
+ }
62
+ function revCount(cwd, range) {
63
+ return parseCount(gitOk(cwd, ["rev-list", "--count", range]));
64
+ }
65
+ function parsePorcelain(text) {
66
+ const entries = [];
67
+ let path = null;
68
+ let branch = null;
69
+ let prunable = false;
70
+ let saw = false;
71
+ const flush = () => {
72
+ if (!saw || path === null)
73
+ return;
74
+ entries.push({ path, branch, prunable });
75
+ path = null;
76
+ branch = null;
77
+ prunable = false;
78
+ saw = false;
79
+ };
80
+ for (const line of text.split("\n")) {
81
+ if (line.startsWith("worktree ")) {
82
+ flush();
83
+ path = line.slice("worktree ".length);
84
+ saw = true;
85
+ continue;
86
+ }
87
+ if (!saw)
88
+ continue;
89
+ if (line.startsWith("branch ")) {
90
+ const ref = line.slice("branch ".length);
91
+ branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
92
+ continue;
93
+ }
94
+ if (line === "detached" || line.startsWith("detached ")) {
95
+ branch = null;
96
+ continue;
97
+ }
98
+ if (line.startsWith("prunable")) {
99
+ prunable = true;
100
+ continue;
101
+ }
102
+ if (line.length === 0)
103
+ flush();
104
+ }
105
+ flush();
106
+ return entries;
107
+ }
108
+ function parseMergedBranches(stdout) {
109
+ const names = new Set();
110
+ for (const line of stdout.split("\n")) {
111
+ const trimmed = line.trim();
112
+ if (trimmed.length === 0)
113
+ continue;
114
+ names.add(trimmed.replace(/^[*+]\s+/, ""));
115
+ }
116
+ return names;
117
+ }
118
+ function readDefaultBase(root) {
119
+ const path = join(root, "tito.yaml");
120
+ let text;
121
+ try {
122
+ text = readFileSync(path, "utf8");
123
+ }
124
+ catch (error) {
125
+ if (errno(error) === "ENOENT")
126
+ return undefined;
127
+ throw new WorktreeError("config", `Cannot read ${path}.`);
128
+ }
129
+ try {
130
+ return parseConfig(text).git?.defaultBase;
131
+ }
132
+ catch (error) {
133
+ if (error instanceof ConfigValidationError)
134
+ throw new WorktreeError("config", error.message);
135
+ throw error;
136
+ }
137
+ }
138
+ function resolveBase(root) {
139
+ const configured = readDefaultBase(root);
140
+ if (configured !== undefined)
141
+ return configured;
142
+ const existing = new Set();
143
+ for (const name of BRANCH_BASES) {
144
+ if (spawnGit(root, ["rev-list", "--count", name]).status === 0)
145
+ existing.add(name);
146
+ }
147
+ return BRANCH_BASES.find((name) => existing.has(name)) ?? "main";
148
+ }
149
+ function unpushedCount(worktreePath) {
150
+ const result = spawnGit(worktreePath, ["rev-list", "--count", "@{upstream}..HEAD"]);
151
+ if (result.status !== 0)
152
+ return null;
153
+ return parseCount(result.stdout);
154
+ }
155
+ function mergedPullRequestListed(stdout) {
156
+ const text = stdout.trim();
157
+ if (text.length === 0 || text === "[]")
158
+ return false;
159
+ try {
160
+ const parsed = JSON.parse(text);
161
+ return Array.isArray(parsed) && parsed.length > 0;
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ }
167
+ function defaultGhMerged(root) {
168
+ let available;
169
+ return (branch) => {
170
+ if (available === undefined) {
171
+ const probe = spawnSync("gh", ["--version"], { encoding: "utf8" });
172
+ available = probe.error === undefined && probe.status === 0;
173
+ }
174
+ if (!available)
175
+ return null;
176
+ const result = spawnSync("gh", ["pr", "list", "--state", "merged", "--head", branch, "--json", "number"], { cwd: root, encoding: "utf8", timeout: 15000 });
177
+ if (result.error || result.status !== 0)
178
+ return false;
179
+ return mergedPullRequestListed(result.stdout ?? "");
180
+ };
181
+ }
182
+ function cherryMerged(root, base, branch) {
183
+ const lines = gitOk(root, ["cherry", base, branch])
184
+ .split("\n")
185
+ .map((line) => line.trim())
186
+ .filter((line) => line.length > 0);
187
+ return lines.length > 0 && lines.every((line) => line.startsWith("-"));
188
+ }
189
+ function branchMoved(root, branch) {
190
+ return spawnGit(root, ["rev-parse", "--verify", "--quiet", `${branch}@{1}`]).status === 0;
191
+ }
192
+ function row(input) {
193
+ return input;
194
+ }
195
+ function classifyEntry(input) {
196
+ const { entry } = input;
197
+ const missing = entry.prunable || !isDirectory(entry.path);
198
+ const behind = entry.branch === null ? 0 : revCount(input.root, `${entry.branch}..${input.base}`);
199
+ const baseFields = {
200
+ path: entry.path,
201
+ branch: entry.branch,
202
+ behind,
203
+ squashMerged: false,
204
+ };
205
+ if (missing)
206
+ return row({ ...baseFields, status: "missing", removable: false });
207
+ const protectedCheckout = isMainCheckout(entry.path) || entry.branch === null || BASE_BRANCHES.has(entry.branch);
208
+ if (protectedCheckout)
209
+ return row({ ...baseFields, status: "protected", removable: false });
210
+ if (gitOk(entry.path, ["status", "--porcelain"]).trim().length > 0) {
211
+ return row({ ...baseFields, status: "dirty", removable: false });
212
+ }
213
+ const unpushed = unpushedCount(entry.path);
214
+ if (unpushed !== null && unpushed > 0) {
215
+ return row({ ...baseFields, status: "unmerged", removable: false });
216
+ }
217
+ const branch = entry.branch;
218
+ if (branch === null)
219
+ return row({ ...baseFields, status: "unmerged", removable: false });
220
+ const contained = input.merged.has(branch);
221
+ if (contained && behind > 0) {
222
+ return row({ ...baseFields, status: "merged-clean", removable: true });
223
+ }
224
+ const pullRequest = input.ghMerged(branch);
225
+ if (pullRequest === true) {
226
+ return row({ ...baseFields, status: "merged-clean", removable: true, squashMerged: true });
227
+ }
228
+ if (pullRequest === null && cherryMerged(input.root, input.base, branch)) {
229
+ return row({ ...baseFields, status: "merged-clean", removable: true });
230
+ }
231
+ if (contained && behind === 0) {
232
+ if (branchMoved(input.root, branch)) {
233
+ return row({ ...baseFields, status: "merged-clean", removable: true });
234
+ }
235
+ return row({ ...baseFields, status: "fresh", removable: false });
236
+ }
237
+ if (behind > 0)
238
+ return row({ ...baseFields, status: "behind", removable: false });
239
+ return row({ ...baseFields, status: "unmerged", removable: false });
240
+ }
241
+ function deleteLocalBranch(root, branch, squashMerged) {
242
+ if (BASE_BRANCHES.has(branch)) {
243
+ throw new WorktreeError("git", `Refusing to delete base branch ${branch}.`);
244
+ }
245
+ const soft = spawnGit(root, ["branch", "-d", branch]);
246
+ if (soft.status === 0)
247
+ return;
248
+ if (!squashMerged) {
249
+ const detail = (soft.stderr || soft.stdout || "git branch -d failed").trim();
250
+ throw new WorktreeError("git", detail);
251
+ }
252
+ gitOk(root, ["branch", "-D", branch]);
253
+ }
254
+ export function tidyWorktrees(input) {
255
+ const root = resolve(input.root);
256
+ if (!isDirectory(root))
257
+ throw new WorktreeError("not-a-directory", `${root} is not a directory.`);
258
+ const base = resolveBase(root);
259
+ const merged = parseMergedBranches(gitOk(root, ["branch", "--merged", base]));
260
+ const ghMerged = input.ghMerged ?? defaultGhMerged(root);
261
+ const rows = parsePorcelain(gitOk(root, ["worktree", "list", "--porcelain"])).map((entry) => classifyEntry({ root, base, entry, merged, ghMerged }));
262
+ const removed = [];
263
+ const deletedBranches = [];
264
+ let pruned = false;
265
+ if (input.confirm === true) {
266
+ for (const item of rows) {
267
+ if (!item.removable || item.branch === null || BASE_BRANCHES.has(item.branch))
268
+ continue;
269
+ if (isMainCheckout(item.path))
270
+ continue;
271
+ gitOk(root, ["worktree", "remove", item.path]);
272
+ removed.push(item.path);
273
+ deleteLocalBranch(root, item.branch, item.squashMerged);
274
+ deletedBranches.push(item.branch);
275
+ }
276
+ if (rows.some((item) => item.status === "missing")) {
277
+ gitOk(root, ["worktree", "prune"]);
278
+ pruned = true;
279
+ }
280
+ }
281
+ return { root, base, rows, removed, deletedBranches, pruned };
282
+ }
283
+ export function formatTidyReport(report) {
284
+ const lines = [`base: ${report.base}`];
285
+ for (const item of report.rows) {
286
+ lines.push(`worktree: ${item.path}`, `branch: ${item.branch ?? "detached"}`, `status: ${item.status}`, `behind: ${item.behind}`, `removable: ${item.removable ? "yes" : "no"}`);
287
+ }
288
+ for (const path of report.removed)
289
+ lines.push(`removed: ${path}`);
290
+ for (const branch of report.deletedBranches)
291
+ lines.push(`deleted-branch: ${branch}`);
292
+ if (report.pruned)
293
+ lines.push("pruned: yes");
294
+ return `${lines.join("\n")}\n`;
295
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anhvupt/tito",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Chat-first engineering coordinator for solo builders. The plan is the spec: decisions, test cases, and docs.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsc",
18
- "test": "npm run build && node --test"
18
+ "test": "npm run build && node --test test/"
19
19
  },
20
20
  "engines": {
21
21
  "node": ">=20"
@@ -0,0 +1,41 @@
1
+ # Tito bootstrap
2
+
3
+ This project uses Tito as its root engineering coordinator.
4
+
5
+ - Start every Tito-coordinated chat response exactly with `Hola, Tito here!`
6
+ The marker is for chat only, never CLI or machine-readable output.
7
+ - Sometimes add one short joke after that greeting. The joke does not replace the answer. Skip it when the user is blocked, when the news is bad, and in CLI or machine-readable output.
8
+ - Treat ordinary user requests as Tito-coordinated work by default.
9
+ - Read `TITO-INITIAL-BRIEF.md` when planning, editing, or resolving policy.
10
+ - Recommend Ask for read-only discovery, Plan for ambiguous or critical work,
11
+ and Agent only for one approved implementation slice.
12
+ - A new command, a new write behavior, or any technical choice is always Plan first. That response is the plan only. No source edits.
13
+ - Skip the plan only when the user clearly instructs that this slice does not need a plan.
14
+ - Implementation starts only after that plan is approved.
15
+ - Confirm intent before clarifying. Questions are only for large decisions; zero is fine.
16
+ - Tito decides small and medium choices and reports them. `client-careful` still asks for medium decisions. `solo-balanced` and `solo-fast` decide both.
17
+ - Tito asks only for large or irreversible choices: product vision or scope, a new command or write behavior, a new dependency, a public CLI or API contract, a data or schema migration, security or permissions, premium-model fan-out, push, a pull request when auto-create is off, merge, deploy, delete, `--confirm` cleanups, and the safety floor.
18
+ - Every update that follows a Tito decision carries one `Decided:` line listing those decisions.
19
+ - Research current official sources and judge them before trusting; adopt maintained tools before building our own; show options with pros and cons for this project.
20
+ - Plans open with Domain (business impact first), then Decisions, Test cases, Docs impact, and Slices and branch. Decisions name conventions, and test cases are tagged AAA.
21
+ - Suggest the git branch and change type, then check out only after the user accepts.
22
+ - Never approve a pull request. Never merge unless the user calls for the merge and the pull request already has an approval.
23
+ - Do not merge a slice branch into `main`, `master`, `dev`, or `develop`, on the machine or on the remote, unless the user calls for that merge and the pull request already has an approval. Finishing a slice leaves the branch as it is.
24
+ - Push directly to the base branch only when the user clearly instructs that push.
25
+ - Tito does not code in this chat. Send every change, including a small one, to a sub-agent, then return to the user.
26
+ - Before writing an approved slice, list installed skills, read only each name and description, and name in the handoff the ones the writer must read.
27
+ - The implementation handoff names the skills it will read.
28
+ - Every sub-agent launch names the model slug from the live catalog.
29
+ - Coding sub-agents follow the profile cap: `client-careful` 3, `solo-balanced` 6, `solo-fast` 12. Each has its own plan and branch. Documentation writers stay one at a time. Check out each branch only after the user accepts that branch.
30
+ - When two or more slices are independent, send those sub-agents together, each on its own branch, up to that cap. Do not line them up one after another when they can run together. After the handoff, return to the user.
31
+ - Preserve uncommitted work.
32
+ - Select specialist advisers and reviewers only when the task needs them. They stay read-only.
33
+ - Stop for human approval between reviewable slices.
34
+ - Never commit, push, publish, deploy, or perform irreversible external actions
35
+ without explicit approval.
36
+ - Use the `/tito` skill when the user invokes it explicitly.
37
+ - Ask before locking a large decision or a product-vision change. One obvious reading continues after a one-line confirmation. After the user answers, the plan records that decision and includes guidance code when it removes implementation ambiguity. Coding agents follow the approved approach.
38
+ - Plans own technical decisions and include guidance code when useful.
39
+
40
+ Keep this bootstrap compact. Load detailed workflow policy from the brief and
41
+ scoped project surfaces only when relevant.
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: Commit subjects and branch names.
3
+ alwaysApply: false
4
+ ---
5
+
6
+ - Bases are `dev`, `develop`, `main`, and `master`. `dev` and `develop` are interchangeable. `main` and `master` are interchangeable.
7
+ - A commit subject is `<type>: <sentence>`. The type is the same token as the branch type: `feat`, `fix`, `hot-fix`, `chores`, `refactor`, or `debug`.
8
+ - The words after the colon are one finished sentence of at most 70 words. The type is not counted in those 70 words. The body is a separate description.
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: Tito init, upgrade, and the opt-in admin index.
3
+ alwaysApply: false
4
+ ---
5
+
6
+ - Init creates `.github/pull_request_template.md` from Tito's template when it is missing. Upgrade replaces that file with Tito's template.
7
+ - In a terminal, `tito init --confirm` asks for the git base, screen language, tenancy, surfaces, and whether to auto-create a pull request after implementation. The auto-pull-request question defaults to yes. Upgrade asks only for settings missing from `tito.yaml`. Storing that answer does not open a pull request.
8
+ - Chat is the primary interface. Implement each operation once, then expose the same behavior as `tito <command>` and `/tito-<command>`.
9
+ - Multi-tenant or multi-surface work includes isolation, consistency, propagation, and permission cases.
10
+
11
+ Per-project Tito install stays. Opt-in `tito admin add|list|remove|refresh`
12
+ registers repos under `~/.config/tito/admin/` and stores commit subject, date,
13
+ and paths — not diffs. A weekly retro stores counts only, never review text.
14
+ A project `tito.yaml` overrides a personal default; do
15
+ not weaken the safety floor.
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Pull request descriptions, review-fix plans, and feedback files.
3
+ globs: .github/pull_request_template.md,.tito/feedback/**/*.md
4
+ alwaysApply: false
5
+ ---
6
+
7
+ - When the user reviews a plan, save Tito's plan and the user's edit as separate files under `.tito/feedback/<slug>/`. `review.md` starts with front matter `outcome: accepted | edited | expanded`.
8
+ - After an approved slice is coded, put every review fix into one plan named `review/<slug>` on the same branch. Ask before opening a pull request only after that plan is coded, or when the user accepts the code with no changes.
9
+ - A pull request description has four parts within 2 to 50 lines: a one-line problem, what changed, review fixes, and checks for lint, code quality, conventions, tests, build, and docs.
10
+ - When the user comments on an open pull request, Tito updates that pull request's description in the same turn. Review fixes lists every comment, including the earlier ones. A thread reply does not replace that update.
11
+ - After a pull request is merged, ask before the next slice. Switch back to the base branch only when the user says so clearly.
12
+ - Docs from the plan are updated or waived before a pull request.
13
+ - The module-end docs writers still run.
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Vietnamese routes and screen text, English screen text, and UTC timestamps.
3
+ alwaysApply: false
4
+ ---
5
+
6
+ - Code stays English.
7
+ - In a Vietnamese app (`screenLanguage: vi`), routes and slugs are Vietnamese first. The public path is native Vietnamese, for example `/tien-ich/ca-phe`. Do not invent that Vietnamese by translating an English slug word for word. If the product is bilingual, the English route comes second.
8
+ - An English app (`screenLanguage: en`) keeps English routes and slugs.
9
+ - In a Vietnamese app (`screenLanguage: vi`), every string a person reads is Vietnamese: tables, labels, buttons, headings, and messages. Write native Vietnamese first. Do not invent it by translating English word for word. English may exist as a second field only when the product is bilingual, and it comes after the Vietnamese.
10
+ - An English app (`screenLanguage: en`) stays English on screen.
11
+ - When `screenLanguage` is missing and the screen language is unclear, Tito asks once. One obvious reading continues without a question.
12
+ - Globalized apps store timestamps in UTC and show them in the user's timezone. There is no switch to turn that off.
@@ -40,7 +40,7 @@ For Plan work, prefer a Reasoning-tier model unless the plan is obvious and
40
40
  bounded or the user chose another model. The implementation agent follows the approved decision and does not invent a new one. Include concise guidance code,
41
41
  signatures, schemas, or pseudocode where it removes ambiguity.
42
42
 
43
- Discover restates the goal and what is out of scope in one or two sentences, then asks whether that is what the user wants, and waits. After the user confirms, ask one batch of 3 to 5 clarifying questions. Each question offers options and a recommended default. `solo-fast` does only the intent check, plus questions about real ambiguity. `client-careful` and `solo-balanced` use the full batch. One obvious reading still continues, after a one-line confirmation. Ask before locking a technical decision or a product-vision change. After the user answers, the plan records that decision and includes guidance code when it removes implementation ambiguity.
43
+ Discover runs the intent check only when the goal is unclear: restate the goal and what is out of scope in one or two sentences, ask whether that is what the user wants, and wait. One obvious reading continues without stopping. Ask clarifying questions only for large decisions, such as product vision or scope, a new dependency, a public contract, a migration, security, or anything irreversible. Each question offers options and a recommended default. Zero questions is fine. Tito decides small and medium choices itself and reports them on one `Decided:` line so the user can overrule them. `client-careful` still asks for medium decisions. After the user answers, the plan records that decision and includes guidance code when it removes implementation ambiguity.
44
44
 
45
45
  The workflow is Discover → Plan → Human Approval → Code → Verify → Docs gate → Review. Every plan has five sections in this order: Domain (opens with one Business impact line, then terms, rules, invariants, and who uses it), Decisions (each names the principle and the project convention it follows, with where that convention lives), Test cases (each has an ID and a layer tag `[unit]`, `[integration]`, or `[e2e]`; behavior tests use Arrange / Act / Assert; edge cases are one line each), Docs impact, and Slices and branch. The approved plan is the spec. For a single slice, the user may say "skip test discussion" or "skip docs", in the same spirit as skipping the plan.
46
46
 
@@ -75,9 +75,9 @@ A plan that touches shared data includes only the kinds that apply:
75
75
  ## Execute
76
76
 
77
77
  1. Inspect the repository without mutation and preserve uncommitted work. When a Tito command exists, use that same core behavior in chat instead of sending the person to the terminal.
78
- 2. Discover: restate the goal and what is out of scope, confirm that reading, then ask the clarifying batch the risk profile calls for. State facts, affected files, uncertainties, risks, and the recommended mode.
78
+ 2. Discover: restate the goal and what is out of scope, confirm that reading only when the goal is unclear, then ask only the large questions the risk profile calls for. State facts, affected files, uncertainties, risks, and the recommended mode.
79
79
  3. For Plan work, produce one plan with Domain, Decisions, Test cases, Docs impact, and Slices and branch, then stop for approval. The approved plan is the spec.
80
- 4. Before Agent work, provide the implementation handoff required by the brief.
80
+ 4. Before Agent work, provide the implementation handoff required by the brief. List installed skills, read only each name and description, and name the ones the writer must read without loading the whole set.
81
81
  5. Tito does not code in this chat. Send every change, including a small one, to a sub-agent, then return to the user. When two or more slices are independent, send those sub-agents together, each on its own branch, up to the profile cap. Do not line them up one after another when they can run together. `client-careful` may run 3 coding sub-agents, `solo-balanced` 6, and `solo-fast` 12. Each has its own plan and branch. Check out each branch only after the user accepts that branch. Documentation writers stay one at a time. Specialist advisers and reviewers stay read-only.
82
82
  6. Code the approved slice by writing the spec doc named in Docs impact first, then the failing tests, then the code. Confirm they fail before implementing until they pass. A test beyond the approved list is flagged as new. A bug fix starts with a test that reproduces the bug. Writing is not committing. Waive the spec doc when the slice changes no behavior.
83
83
  7. Verify: report each approved test ID as passing or failing, plus lint and build.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: tito-worktree-tidy
3
+ description: Report stale git worktrees and remove merged clean ones when confirmed. Invoke explicitly with /tito-worktree-tidy. Uses the same report as tito worktree tidy.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Tito worktree tidy
8
+
9
+ Chat adapter for `npx tito worktree tidy`. Run that command and show its stdout unchanged.
10
+ Start the chat response exactly with `Hola, Tito here!` The greeting is not part of the command output.
11
+
12
+ Add `--root <path>` only when the user names another directory. Add `--confirm` only when the user asks to remove merged clean worktrees. Do not reimplement tidy by running git yourself. Do not delete remote branches. Do not rebase. Do not remove the main checkout, a base branch, or a worktree with uncommitted or unpushed work.
13
+
14
+ After a pull request is merged, Tito first runs `tito worktree tidy` report-only, then asks before the next slice.