@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.
- package/README.md +31 -5
- package/dist/cli.js +118 -3
- package/dist/core/agents.js +6 -4
- package/dist/core/config.js +58 -1
- package/dist/core/git-flow.js +71 -0
- package/dist/core/init.js +22 -5
- package/dist/core/models.js +117 -38
- package/dist/core/plan.js +22 -20
- package/dist/core/skills.js +64 -0
- package/dist/core/specialists.js +14 -1
- package/dist/core/test-first.js +95 -0
- package/dist/core/upgrade.js +40 -5
- package/dist/core/work-plan.js +17 -0
- package/dist/core/worktree.js +295 -0
- package/package.json +2 -2
- package/templates/cursor/AGENTS.md +41 -0
- package/templates/cursor/rules/commit-branch.mdc +8 -0
- package/templates/cursor/rules/init-upgrade-admin.mdc +15 -0
- package/templates/cursor/rules/pull-request.mdc +13 -0
- package/templates/cursor/rules/screen-language.mdc +12 -0
- package/templates/cursor/skills/tito/SKILL.md +3 -3
- package/templates/cursor/skills/tito-worktree-tidy/SKILL.md +14 -0
package/README.md
CHANGED
|
@@ -19,9 +19,11 @@ specialists, and it updates the docs named in the plan before a pull request.
|
|
|
19
19
|
uncertain or critical work, and Agent only for an approved implementation
|
|
20
20
|
slice.
|
|
21
21
|
- **Plans that make decisions:** Tito confirms the goal and what is out of
|
|
22
|
-
scope, then asks
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
scope, then asks only the questions that are large; zero is fine.
|
|
23
|
+
`client-careful` still asks for medium decisions. `solo-balanced` and
|
|
24
|
+
`solo-fast` decide small and medium choices and report them on a `Decided:`
|
|
25
|
+
line. One obvious reading continues after a one-line confirmation. Tito asks
|
|
26
|
+
before locking a large decision or a product-vision change. The plan
|
|
25
27
|
records that decision and includes signatures, schemas, pseudocode, or
|
|
26
28
|
guidance code when that makes implementation clearer. Decisions name the
|
|
27
29
|
convention they follow. Test cases carry a layer tag: `[unit]`,
|
|
@@ -66,6 +68,7 @@ Several read-only specialists may work together. Coding sub-agents follow the
|
|
|
66
68
|
profile cap: `client-careful` 3, `solo-balanced` 6, and `solo-fast` 12. Each
|
|
67
69
|
has its own plan and branch. Tito does not code in the chat. Every change,
|
|
68
70
|
including a small one, goes to a sub-agent, and Tito returns to the user.
|
|
71
|
+
Before writing an approved slice, Tito lists installed skills, reads only each name and description, and names in the handoff the ones the writer must read.
|
|
69
72
|
When two or more slices are independent, Tito sends those sub-agents together,
|
|
70
73
|
each on its own branch, up to that cap, and does not line them up one after
|
|
71
74
|
another. Check out each branch only after you accept that branch.
|
|
@@ -107,7 +110,9 @@ The first `init` prints the plan and writes nothing. In a terminal, omitting
|
|
|
107
110
|
`--profile` asks you to choose `client-careful`, `solo-balanced`, or
|
|
108
111
|
`solo-fast`. A script or piped command must still pass `--profile`.
|
|
109
112
|
`--confirm` creates the missing `tito.yaml`, specialist agents, and `/tito`
|
|
110
|
-
skills.
|
|
113
|
+
skills. It also installs Tito's scoped rules under `.cursor/rules/`
|
|
114
|
+
(pull-request, screen-language, init-upgrade-admin, and commit-branch). If
|
|
115
|
+
`AGENTS.md` already exists, Tito appends its bootstrap and leaves
|
|
111
116
|
the existing guidance in place. It refuses to overwrite a Tito file that is
|
|
112
117
|
already there. In a terminal, `tito init --confirm` then asks for the git base
|
|
113
118
|
(`dev`, `develop`, `main`, or `master`), screen language (`vi` or `en`),
|
|
@@ -130,7 +135,8 @@ npx tito upgrade --confirm
|
|
|
130
135
|
```
|
|
131
136
|
|
|
132
137
|
`--confirm` installs the latest `@anhvupt/tito` and replaces Tito-owned
|
|
133
|
-
agents, skills, and the Tito section of
|
|
138
|
+
agents, skills, those Tito-owned scoped rules, and the Tito section of
|
|
139
|
+
`AGENTS.md`. Consumer rules, consumer
|
|
134
140
|
agents, and other project skills stay untouched. In a terminal, `tito upgrade --confirm` asks only for settings missing from `tito.yaml`: git base, screen language, tenancy, surfaces, and auto-create pull request. Enter on that last question still stores yes. It does not change a value that is already present, and it does not change the profile. A non-interactive upgrade does not ask. If `tito.yaml` is missing, it skips those questions.
|
|
135
141
|
|
|
136
142
|
Tito is installed in each project. It is not a required global command.
|
|
@@ -178,6 +184,9 @@ node dist/cli.js admin list
|
|
|
178
184
|
node dist/cli.js admin refresh
|
|
179
185
|
node dist/cli.js admin remove --root /path/to/repo
|
|
180
186
|
node dist/cli.js admin retro --week 2026-W40
|
|
187
|
+
node dist/cli.js guard test-first --base main --test test/example.test.mjs
|
|
188
|
+
node dist/cli.js worktree tidy
|
|
189
|
+
node dist/cli.js worktree tidy --confirm
|
|
181
190
|
```
|
|
182
191
|
|
|
183
192
|
`inspect` reads `tito.yaml` when it exists and checks whether `AGENTS.md` is
|
|
@@ -196,10 +205,16 @@ registered paths, `remove` drops one path, and `refresh` writes local branch plu
|
|
|
196
205
|
the last 50 commit subjects, dates, and file paths (secrets like `.env` skipped;
|
|
197
206
|
no diffs) for chat and CLI to share.
|
|
198
207
|
|
|
208
|
+
`worktree tidy` (chat: `/tito-worktree-tidy`) reports each linked worktree as protected, merged-clean, dirty, unmerged, behind, fresh, or missing. It removes a merged clean worktree and its local branch only with `--confirm`. It does not delete remote branches or rebase.
|
|
209
|
+
|
|
199
210
|
`retro` builds that week's report from local git and saves `retros/<week>.json`
|
|
200
211
|
and `retros/<week>.md` under the admin root. Review-fix counts, docs-blank, and
|
|
201
212
|
merge duration say unavailable until GitHub data is added.
|
|
202
213
|
|
|
214
|
+
`guard test-first` checks a slice branch against `--base`. Commit 1 may change
|
|
215
|
+
only the repeated `--test` files. Later commits may not change `test/**`,
|
|
216
|
+
`*.test.*`, or `*.spec.*` files. A failure names the file and the commit.
|
|
217
|
+
|
|
203
218
|
## How work moves
|
|
204
219
|
|
|
205
220
|
The normal careful path is:
|
|
@@ -240,6 +255,17 @@ product:
|
|
|
240
255
|
- id: admin
|
|
241
256
|
```
|
|
242
257
|
|
|
258
|
+
Optional `models` maps pipeline roles to Cursor model slugs. Each Tito specialist card compiles to one role: explorer, planner, implementer, reviewer, or finisher. Missing keys fall back to the defaults below. Empty or unknown keys are rejected. `tito init` writes the full block; `tito upgrade` adds missing roles without changing values you already set, and rewrites Tito-owned specialist cards so pinned slugs stay current.
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
models:
|
|
262
|
+
explorer: cursor-grok-4.6-medium-fast
|
|
263
|
+
planner: claude-opus-5-5-medium
|
|
264
|
+
implementer: grok-4.7-high
|
|
265
|
+
reviewer: grok-4.7-high
|
|
266
|
+
finisher: claude-sonnet-5-5-high
|
|
267
|
+
```
|
|
268
|
+
|
|
243
269
|
- Code stays English.
|
|
244
270
|
- 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.
|
|
245
271
|
- An English app (`screenLanguage: en`) keeps English routes and slugs.
|
package/dist/cli.js
CHANGED
|
@@ -9,10 +9,12 @@ import { InspectionError, formatInspection, inspectRepository, } from "./core/in
|
|
|
9
9
|
import { PlanError, formatAdoptionPlan, planAdoption } from "./core/plan.js";
|
|
10
10
|
import { ConfigValidationError, parseConfig } from "./core/config.js";
|
|
11
11
|
import { INIT_SETTING_KEYS, InitError, applyInitialization, formatInitialization, planInitialization, promptForProfile, promptForSettings, } from "./core/init.js";
|
|
12
|
-
import { applyUpgrade, formatUpgrade, mergeSettings, missingSettings, planUpgrade, titoOwnedFiles, } from "./core/upgrade.js";
|
|
12
|
+
import { applyUpgrade, formatUpgrade, mergeSettings, missingSettings, planUpgrade, projectModels, titoOwnedFiles, } from "./core/upgrade.js";
|
|
13
13
|
import { AdminError, addRepo, formatContexts, formatRepos, listRepos, refreshRepos, removeRepo, resolveAdminRoot, } from "./core/admin.js";
|
|
14
14
|
import { RetroError, buildRetro, formatRetro, retroJson, saveRetro, } from "./core/retro.js";
|
|
15
15
|
import { formatInitError } from "./core/terminal.js";
|
|
16
|
+
import { TestFirstError, formatTestFirstReport, guardTestFirst, } from "./core/test-first.js";
|
|
17
|
+
import { WorktreeError, formatTidyReport, tidyWorktrees } from "./core/worktree.js";
|
|
16
18
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
17
19
|
const help = `Tito — quiet orchestration for solo builders
|
|
18
20
|
|
|
@@ -27,6 +29,8 @@ Usage:
|
|
|
27
29
|
tito admin remove --root <path> [--admin-root <path>]
|
|
28
30
|
tito admin refresh [--admin-root <path>]
|
|
29
31
|
tito admin retro [--week <YYYY-Www>] [--timezone <IANA>] [--json] [--admin-root <path>]
|
|
32
|
+
tito guard test-first --base <ref> --test <file> [--root <path>]
|
|
33
|
+
tito worktree tidy [--confirm] [--root <path>]
|
|
30
34
|
|
|
31
35
|
Options:
|
|
32
36
|
-h, --help Show help
|
|
@@ -38,6 +42,8 @@ Commands:
|
|
|
38
42
|
init Install Tito files and specialist agents. Confirm before writing.
|
|
39
43
|
upgrade Install the latest Tito and replace Tito-owned files only.
|
|
40
44
|
admin Opt-in local repo index: add, list, remove, refresh, retro.
|
|
45
|
+
guard Check that commit 1 changes only the listed test files.
|
|
46
|
+
worktree Report stale worktrees. Confirm before removing merged clean ones.
|
|
41
47
|
`;
|
|
42
48
|
function fail(message) {
|
|
43
49
|
process.stderr.write(`${message}\n`);
|
|
@@ -239,13 +245,15 @@ async function runUpgrade(args) {
|
|
|
239
245
|
fail(error instanceof Error ? error.message : "Invalid upgrade arguments.");
|
|
240
246
|
return;
|
|
241
247
|
}
|
|
242
|
-
|
|
248
|
+
let files = planUpgrade(root, titoOwnedFiles(projectModels(root)));
|
|
243
249
|
if (!confirm) {
|
|
244
250
|
process.stdout.write(formatUpgrade(root, files));
|
|
245
251
|
return;
|
|
246
252
|
}
|
|
247
|
-
if (!filesOnly && input.isTTY)
|
|
253
|
+
if (!filesOnly && input.isTTY) {
|
|
248
254
|
await fillMissingSettings(root);
|
|
255
|
+
files = planUpgrade(root, titoOwnedFiles(projectModels(root)));
|
|
256
|
+
}
|
|
249
257
|
if (!filesOnly && !isTitoSource(root)) {
|
|
250
258
|
const install = spawnSync("npm", ["install", "-D", "@anhvupt/tito@latest"], { cwd: root, encoding: "utf8" });
|
|
251
259
|
if (install.status !== 0) {
|
|
@@ -421,6 +429,105 @@ function runRetro(args) {
|
|
|
421
429
|
throw error;
|
|
422
430
|
}
|
|
423
431
|
}
|
|
432
|
+
function runGuard(args) {
|
|
433
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
434
|
+
process.stdout.write(help);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const [subcommand, ...rest] = args;
|
|
438
|
+
if (subcommand !== "test-first") {
|
|
439
|
+
fail("Unknown guard. Use: tito guard test-first --base <ref> --test <file>");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
let root = process.cwd();
|
|
443
|
+
let base;
|
|
444
|
+
let testFiles = [];
|
|
445
|
+
try {
|
|
446
|
+
const { values } = parseArgs({
|
|
447
|
+
args: rest,
|
|
448
|
+
options: {
|
|
449
|
+
base: { type: "string" },
|
|
450
|
+
root: { type: "string" },
|
|
451
|
+
test: { type: "string", multiple: true },
|
|
452
|
+
},
|
|
453
|
+
strict: true,
|
|
454
|
+
allowPositionals: false,
|
|
455
|
+
});
|
|
456
|
+
if (values.base !== undefined)
|
|
457
|
+
base = values.base;
|
|
458
|
+
if (values.root !== undefined)
|
|
459
|
+
root = values.root;
|
|
460
|
+
if (Array.isArray(values.test))
|
|
461
|
+
testFiles = values.test;
|
|
462
|
+
else if (typeof values.test === "string")
|
|
463
|
+
testFiles = [values.test];
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
fail(error instanceof Error ? error.message : "Invalid guard arguments.");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (base === undefined) {
|
|
470
|
+
fail("Missing required option --base.");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (testFiles.length === 0) {
|
|
474
|
+
fail("Missing required option --test.");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const report = guardTestFirst({ root, base, testFiles });
|
|
479
|
+
process.stdout.write(formatTestFirstReport(report));
|
|
480
|
+
if (!report.ok)
|
|
481
|
+
process.exitCode = 1;
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (error instanceof TestFirstError) {
|
|
485
|
+
fail(`${error.code}: ${error.message}`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function runWorktree(args) {
|
|
492
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
493
|
+
process.stdout.write(help);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (args[0] !== "tidy") {
|
|
497
|
+
fail("Unknown worktree command. Use tito worktree tidy.");
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
let root = process.cwd();
|
|
501
|
+
let confirm = false;
|
|
502
|
+
try {
|
|
503
|
+
const { values } = parseArgs({
|
|
504
|
+
args: args.slice(1),
|
|
505
|
+
options: {
|
|
506
|
+
confirm: { type: "boolean" },
|
|
507
|
+
root: { type: "string" },
|
|
508
|
+
},
|
|
509
|
+
strict: true,
|
|
510
|
+
allowPositionals: false,
|
|
511
|
+
});
|
|
512
|
+
confirm = values.confirm === true;
|
|
513
|
+
if (values.root !== undefined)
|
|
514
|
+
root = values.root;
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
fail(error instanceof Error ? error.message : "Invalid worktree arguments.");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
process.stdout.write(formatTidyReport(tidyWorktrees({ root, confirm })));
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
if (error instanceof WorktreeError) {
|
|
525
|
+
fail(`${error.code}: ${error.message}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
424
531
|
async function run(args) {
|
|
425
532
|
if (args[0] === "inspect") {
|
|
426
533
|
runInspect(args.slice(1));
|
|
@@ -440,6 +547,14 @@ async function run(args) {
|
|
|
440
547
|
runAdmin(args.slice(1));
|
|
441
548
|
return;
|
|
442
549
|
}
|
|
550
|
+
if (args[0] === "guard") {
|
|
551
|
+
runGuard(args.slice(1));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (args[0] === "worktree") {
|
|
555
|
+
runWorktree(args.slice(1));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
443
558
|
let helpRequested = false;
|
|
444
559
|
let versionRequested = false;
|
|
445
560
|
try {
|
package/dist/core/agents.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveModels } from "./config.js";
|
|
1
2
|
import { SPECIALIST_IDS, SPECIALISTS } from "./specialists.js";
|
|
2
3
|
const AGENT_FILE_NAMES = {
|
|
3
4
|
explorer: "tito-explorer.md",
|
|
@@ -15,8 +16,9 @@ const AGENT_FILE_NAMES = {
|
|
|
15
16
|
export function cursorAgentPath(id) {
|
|
16
17
|
return `.cursor/agents/${AGENT_FILE_NAMES[id]}`;
|
|
17
18
|
}
|
|
18
|
-
export function compileCursorAgent(id) {
|
|
19
|
+
export function compileCursorAgent(id, models) {
|
|
19
20
|
const manifest = SPECIALISTS[id];
|
|
21
|
+
const model = resolveModels(models)[manifest.model.role];
|
|
20
22
|
const readOnly = manifest.modes.every((mode) => mode.capability === "read-only");
|
|
21
23
|
const modes = manifest.modes
|
|
22
24
|
.map((mode) => `- \`${mode.id}\`: ${mode.capability}`)
|
|
@@ -32,7 +34,7 @@ name: ${AGENT_FILE_NAMES[id].replace(/\.md$/, "")}
|
|
|
32
34
|
description: >
|
|
33
35
|
${manifest.role}. Tito delegates this specialist only for: ${manifest.triggers.join("; ")}.
|
|
34
36
|
You are not the root coordinator. Follow the assigned mode and stop condition.
|
|
35
|
-
model:
|
|
37
|
+
model: ${model}
|
|
36
38
|
readonly: ${readOnly}
|
|
37
39
|
---
|
|
38
40
|
|
|
@@ -61,9 +63,9 @@ Return the result to Tito. Suggest another specialist only from this manifest's
|
|
|
61
63
|
${manifest.stopCondition}
|
|
62
64
|
`;
|
|
63
65
|
}
|
|
64
|
-
export function compiledCursorAgents() {
|
|
66
|
+
export function compiledCursorAgents(models) {
|
|
65
67
|
return SPECIALIST_IDS.map((id) => ({
|
|
66
68
|
path: cursorAgentPath(id),
|
|
67
|
-
body: compileCursorAgent(id),
|
|
69
|
+
body: compileCursorAgent(id, models),
|
|
68
70
|
}));
|
|
69
71
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -2,12 +2,23 @@ import { isMap, isScalar, isSeq, parseAllDocuments } from "yaml";
|
|
|
2
2
|
import { BRANCH_BASES } from "./git-flow.js";
|
|
3
3
|
import { ACTIVE_RISK_PROFILE_IDS, isActiveRiskProfileId, isFutureRiskProfileId, } from "./profiles.js";
|
|
4
4
|
export const CONFIG_SCHEMA_VERSION = 1;
|
|
5
|
-
const FIELDS = new Set(["schemaVersion", "profile", "git", "product"]);
|
|
5
|
+
const FIELDS = new Set(["schemaVersion", "profile", "git", "product", "models"]);
|
|
6
|
+
export const MODEL_ROLES = ["explorer", "planner", "implementer", "reviewer", "finisher"];
|
|
7
|
+
export const DEFAULT_MODELS = Object.freeze({
|
|
8
|
+
explorer: "cursor-grok-4.6-medium-fast",
|
|
9
|
+
planner: "claude-opus-5-5-medium",
|
|
10
|
+
implementer: "grok-4.7-high",
|
|
11
|
+
reviewer: "grok-4.7-high",
|
|
12
|
+
finisher: "claude-sonnet-5-5-high",
|
|
13
|
+
});
|
|
6
14
|
export const SCREEN_LANGUAGES = ["vi", "en"];
|
|
7
15
|
export const TENANCY_VALUES = ["single", "multi"];
|
|
8
16
|
const PRODUCT_FIELDS = new Set(["screenLanguage", "tenancy", "surfaces"]);
|
|
9
17
|
const SURFACE_ID = /^[a-z][a-z0-9-]*$/;
|
|
10
18
|
const NON_SCALAR = Symbol("non-scalar");
|
|
19
|
+
export function resolveModels(models) {
|
|
20
|
+
return { ...DEFAULT_MODELS, ...models };
|
|
21
|
+
}
|
|
11
22
|
export class ConfigValidationError extends Error {
|
|
12
23
|
issues;
|
|
13
24
|
constructor(issues) {
|
|
@@ -226,6 +237,42 @@ function readProduct(node) {
|
|
|
226
237
|
const product = assembleProduct({ screenLanguage, tenancy, surfaces });
|
|
227
238
|
return product ? { product, issues } : { issues };
|
|
228
239
|
}
|
|
240
|
+
function isModelRole(value) {
|
|
241
|
+
return MODEL_ROLES.includes(value);
|
|
242
|
+
}
|
|
243
|
+
function readModels(node) {
|
|
244
|
+
if (!isMap(node)) {
|
|
245
|
+
return { issues: [issue("unknown-field", 'Field "models" must be a mapping.', "models")] };
|
|
246
|
+
}
|
|
247
|
+
const issues = [];
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
const models = {};
|
|
250
|
+
for (const item of node.items) {
|
|
251
|
+
const key = mappingKey(item.key);
|
|
252
|
+
if (key === null) {
|
|
253
|
+
issues.push(issue("unknown-field", "Mapping keys must be strings.", "models"));
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (seen.has(key)) {
|
|
257
|
+
issues.push(issue("duplicate-key", `Duplicate key "${key}".`, `models.${key}`));
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
seen.add(key);
|
|
261
|
+
if (!isModelRole(key)) {
|
|
262
|
+
issues.push(issue("unknown-field", `Unknown field "models.${key}".`, `models.${key}`));
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const value = isScalar(item.value) ? item.value.value : NON_SCALAR;
|
|
266
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
267
|
+
issues.push(issue("unknown-field", `Field "models.${key}" must be a non-empty model slug.`, `models.${key}`));
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
models[key] = value.trim();
|
|
271
|
+
}
|
|
272
|
+
if (issues.length > 0)
|
|
273
|
+
return { issues };
|
|
274
|
+
return Object.keys(models).length > 0 ? { models, issues } : { issues };
|
|
275
|
+
}
|
|
229
276
|
function profileIssue(value) {
|
|
230
277
|
if (typeof value !== "string") {
|
|
231
278
|
return issue("unknown-profile", "Profile must be a string.", "profile");
|
|
@@ -268,6 +315,7 @@ export function parseConfig(text) {
|
|
|
268
315
|
const values = new Map();
|
|
269
316
|
let git;
|
|
270
317
|
let product;
|
|
318
|
+
let models;
|
|
271
319
|
for (const item of document.contents.items) {
|
|
272
320
|
const key = isScalar(item.key) && typeof item.key.value === "string" ? item.key.value : null;
|
|
273
321
|
if (key === null) {
|
|
@@ -298,6 +346,13 @@ export function parseConfig(text) {
|
|
|
298
346
|
product = parsed.product;
|
|
299
347
|
continue;
|
|
300
348
|
}
|
|
349
|
+
if (key === "models") {
|
|
350
|
+
const parsed = readModels(item.value);
|
|
351
|
+
issues.push(...parsed.issues);
|
|
352
|
+
if (parsed.models)
|
|
353
|
+
models = parsed.models;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
301
356
|
values.set(key, isScalar(item.value) ? item.value.value : NON_SCALAR);
|
|
302
357
|
}
|
|
303
358
|
if ((occurrences.get("schemaVersion") ?? 0) === 0) {
|
|
@@ -326,5 +381,7 @@ export function parseConfig(text) {
|
|
|
326
381
|
result.git = git;
|
|
327
382
|
if (product)
|
|
328
383
|
result.product = product;
|
|
384
|
+
if (models)
|
|
385
|
+
result.models = models;
|
|
329
386
|
return result;
|
|
330
387
|
}
|
package/dist/core/git-flow.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recommendModel } from "./models.js";
|
|
1
2
|
export const BRANCH_TYPES = [
|
|
2
3
|
"feat",
|
|
3
4
|
"fix",
|
|
@@ -172,3 +173,73 @@ export function mergeDecision(input) {
|
|
|
172
173
|
export function canApprovePullRequest() {
|
|
173
174
|
return false;
|
|
174
175
|
}
|
|
176
|
+
const PROTECTED_PUSH_BRANCHES = new Set(["main", "master", "dev", "develop"]);
|
|
177
|
+
const SLICE_CHECKS = ["guard", "lint", "tests", "build", "docs"];
|
|
178
|
+
export function finishSlice(input) {
|
|
179
|
+
if (input.role !== "finisher") {
|
|
180
|
+
return {
|
|
181
|
+
status: "refused",
|
|
182
|
+
pushed: false,
|
|
183
|
+
reason: `The ${input.role} may not open a pull request.`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (PROTECTED_PUSH_BRANCHES.has(input.branch)) {
|
|
187
|
+
return {
|
|
188
|
+
status: "refused",
|
|
189
|
+
pushed: false,
|
|
190
|
+
reason: `Refusing to push ${input.branch}.`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (input.autoPullRequest !== true) {
|
|
194
|
+
return {
|
|
195
|
+
status: "ask",
|
|
196
|
+
pushed: false,
|
|
197
|
+
reason: "Tito asks before opening a pull request.",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
for (const check of SLICE_CHECKS) {
|
|
201
|
+
if (input.checks[check] !== true) {
|
|
202
|
+
return {
|
|
203
|
+
status: "failed",
|
|
204
|
+
pushed: false,
|
|
205
|
+
check,
|
|
206
|
+
reason: `The ${check} check failed. Nothing was pushed.`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const recommendation = recommendModel({
|
|
211
|
+
host: "cursor",
|
|
212
|
+
role: "finisher",
|
|
213
|
+
catalog: input.catalog,
|
|
214
|
+
});
|
|
215
|
+
if (recommendation.status !== "selected") {
|
|
216
|
+
return {
|
|
217
|
+
status: "ask",
|
|
218
|
+
pushed: false,
|
|
219
|
+
reason: recommendation.reason,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const body = pullRequestDescription(input.description);
|
|
223
|
+
if (body === null) {
|
|
224
|
+
return {
|
|
225
|
+
status: "failed",
|
|
226
|
+
pushed: false,
|
|
227
|
+
reason: "The pull request description is not a valid four-part description.",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
input.adapter.push(input.branch);
|
|
231
|
+
const opened = input.adapter.openPullRequest({
|
|
232
|
+
head: input.branch,
|
|
233
|
+
base: input.base,
|
|
234
|
+
title: input.description.problem.trim(),
|
|
235
|
+
body,
|
|
236
|
+
readyForReview: true,
|
|
237
|
+
});
|
|
238
|
+
return {
|
|
239
|
+
status: "opened",
|
|
240
|
+
model: recommendation.model,
|
|
241
|
+
url: opened.url,
|
|
242
|
+
pushed: true,
|
|
243
|
+
readyForReview: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
package/dist/core/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSy
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { compiledCursorAgents } from "./agents.js";
|
|
5
|
-
import { SCREEN_LANGUAGES, TENANCY_VALUES, isSurfaceId, } from "./config.js";
|
|
5
|
+
import { MODEL_ROLES, SCREEN_LANGUAGES, TENANCY_VALUES, isSurfaceId, resolveModels, } from "./config.js";
|
|
6
6
|
import { BRANCH_BASES, PULL_REQUEST_TEMPLATE_PATH, pullRequestTemplate, } from "./git-flow.js";
|
|
7
7
|
import { ACTIVE_RISK_PROFILE_IDS } from "./profiles.js";
|
|
8
8
|
import { PlanError, TITO_BOOTSTRAP_START, planAdoption, titoBootstrapBlock, } from "./plan.js";
|
|
@@ -123,6 +123,8 @@ export function titoYamlBody(profile, settings) {
|
|
|
123
123
|
}
|
|
124
124
|
if (productLines.length > 0)
|
|
125
125
|
lines.push("product:", ...productLines);
|
|
126
|
+
const models = resolveModels(settings.models);
|
|
127
|
+
lines.push("models:", ...MODEL_ROLES.map((role) => ` ${role}: ${models[role]}`));
|
|
126
128
|
return `${lines.join("\n")}\n`;
|
|
127
129
|
}
|
|
128
130
|
function exists(root, path) {
|
|
@@ -142,6 +144,17 @@ export function shippedSkills() {
|
|
|
142
144
|
body: readFileSync(join(root, entry.name, "SKILL.md"), "utf8"),
|
|
143
145
|
}));
|
|
144
146
|
}
|
|
147
|
+
export function shippedCursorRules() {
|
|
148
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "../../templates/cursor/rules");
|
|
149
|
+
return readdirSync(root, { withFileTypes: true })
|
|
150
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".mdc"))
|
|
151
|
+
.map((entry) => entry.name)
|
|
152
|
+
.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))
|
|
153
|
+
.map((name) => ({
|
|
154
|
+
path: `.cursor/rules/${name}`,
|
|
155
|
+
body: readFileSync(join(root, name), "utf8"),
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
145
158
|
function agentsBootstrap(report) {
|
|
146
159
|
if (report.agentsMd === "absent") {
|
|
147
160
|
return { path: "AGENTS.md", action: "create", body: titoBootstrapBlock() };
|
|
@@ -162,16 +175,17 @@ export function planInitialization(report, profile, settings) {
|
|
|
162
175
|
const files = adoption.files.map((file) => {
|
|
163
176
|
if (file.path === "AGENTS.md")
|
|
164
177
|
return agentsBootstrap(report);
|
|
165
|
-
if (file.path === "tito.yaml" && file.action === "create"
|
|
178
|
+
if (file.path === "tito.yaml" && file.action === "create") {
|
|
166
179
|
return {
|
|
167
180
|
path: "tito.yaml",
|
|
168
181
|
action: "create",
|
|
169
|
-
body: titoYamlBody(adoption.profile, settings),
|
|
182
|
+
body: titoYamlBody(adoption.profile, settings ?? {}),
|
|
170
183
|
};
|
|
171
184
|
}
|
|
172
185
|
return file;
|
|
173
186
|
});
|
|
174
|
-
const
|
|
187
|
+
const models = report.titoYaml.status === "valid" ? report.titoYaml.config.models : settings?.models;
|
|
188
|
+
const agents = compiledCursorAgents(models).map((agent) => exists(report.root, agent.path)
|
|
175
189
|
? { path: agent.path, action: "conflict" }
|
|
176
190
|
: { path: agent.path, action: "create", body: agent.body });
|
|
177
191
|
const skills = shippedSkills().map((skill) => exists(report.root, skill.path)
|
|
@@ -181,10 +195,13 @@ export function planInitialization(report, profile, settings) {
|
|
|
181
195
|
const pullRequest = exists(report.root, PULL_REQUEST_TEMPLATE_PATH)
|
|
182
196
|
? { path: PULL_REQUEST_TEMPLATE_PATH, action: "keep" }
|
|
183
197
|
: { path: PULL_REQUEST_TEMPLATE_PATH, action: "create", body: template };
|
|
198
|
+
const rules = shippedCursorRules().map((rule) => exists(report.root, rule.path)
|
|
199
|
+
? { path: rule.path, action: "keep" }
|
|
200
|
+
: { path: rule.path, action: "create", body: rule.body });
|
|
184
201
|
return {
|
|
185
202
|
root: adoption.root,
|
|
186
203
|
profile: adoption.profile,
|
|
187
|
-
files: [...files, ...agents, ...skills, pullRequest],
|
|
204
|
+
files: [...files, ...agents, ...skills, pullRequest, ...rules],
|
|
188
205
|
};
|
|
189
206
|
}
|
|
190
207
|
export function formatInitialization(plan) {
|