@anhvupt/tito 0.1.2 → 0.2.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.
- package/README.md +88 -17
- package/dist/cli.js +203 -0
- package/dist/core/admin.js +241 -0
- package/dist/core/config.js +185 -2
- package/dist/core/feedback.js +68 -0
- package/dist/core/git-flow.js +174 -0
- package/dist/core/init.js +16 -6
- package/dist/core/plan.js +6 -0
- package/dist/core/profiles.js +6 -6
- package/dist/core/retro.js +444 -0
- package/dist/core/specialists.js +14 -8
- package/dist/core/upgrade.js +77 -0
- package/dist/core/work-plan.js +6 -5
- package/package.json +2 -2
- package/templates/cursor/skills/tito/SKILL.md +64 -11
- package/templates/cursor/skills/tito-admin-retro/SKILL.md +12 -0
package/dist/core/config.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { isMap, isScalar, parseAllDocuments } from "yaml";
|
|
1
|
+
import { isMap, isScalar, isSeq, parseAllDocuments } from "yaml";
|
|
2
|
+
import { BRANCH_BASES } from "./git-flow.js";
|
|
2
3
|
import { ACTIVE_RISK_PROFILE_IDS, isActiveRiskProfileId, isFutureRiskProfileId, } from "./profiles.js";
|
|
3
4
|
export const CONFIG_SCHEMA_VERSION = 1;
|
|
4
|
-
const FIELDS = new Set(["schemaVersion", "profile"]);
|
|
5
|
+
const FIELDS = new Set(["schemaVersion", "profile", "git", "product"]);
|
|
6
|
+
const SCREEN_LANGUAGES = ["vi", "en"];
|
|
7
|
+
const TENANCY_VALUES = ["single", "multi"];
|
|
8
|
+
const PRODUCT_FIELDS = new Set(["screenLanguage", "tenancy", "surfaces"]);
|
|
9
|
+
const SURFACE_ID = /^[a-z][a-z0-9-]*$/;
|
|
5
10
|
const NON_SCALAR = Symbol("non-scalar");
|
|
6
11
|
export class ConfigValidationError extends Error {
|
|
7
12
|
issues;
|
|
@@ -25,6 +30,158 @@ function shown(value) {
|
|
|
25
30
|
}
|
|
26
31
|
return "a non-scalar value";
|
|
27
32
|
}
|
|
33
|
+
function isBranchBase(value) {
|
|
34
|
+
return typeof value === "string" && BRANCH_BASES.includes(value);
|
|
35
|
+
}
|
|
36
|
+
function readGitBase(node) {
|
|
37
|
+
if (!isMap(node))
|
|
38
|
+
return "invalid";
|
|
39
|
+
let base = null;
|
|
40
|
+
for (const item of node.items) {
|
|
41
|
+
const key = isScalar(item.key) && typeof item.key.value === "string" ? item.key.value : null;
|
|
42
|
+
if (key !== "defaultBase")
|
|
43
|
+
return "invalid";
|
|
44
|
+
const value = isScalar(item.value) ? item.value.value : null;
|
|
45
|
+
base = isBranchBase(value) ? value : "invalid";
|
|
46
|
+
}
|
|
47
|
+
return base;
|
|
48
|
+
}
|
|
49
|
+
function isScreenLanguage(value) {
|
|
50
|
+
return typeof value === "string" && SCREEN_LANGUAGES.includes(value);
|
|
51
|
+
}
|
|
52
|
+
function isTenancy(value) {
|
|
53
|
+
return typeof value === "string" && TENANCY_VALUES.includes(value);
|
|
54
|
+
}
|
|
55
|
+
function mappingKey(key) {
|
|
56
|
+
return isScalar(key) && typeof key.value === "string" ? key.value : null;
|
|
57
|
+
}
|
|
58
|
+
function assembleProduct(parts) {
|
|
59
|
+
const product = {};
|
|
60
|
+
if (parts.screenLanguage !== undefined)
|
|
61
|
+
product.screenLanguage = parts.screenLanguage;
|
|
62
|
+
if (parts.tenancy !== undefined)
|
|
63
|
+
product.tenancy = parts.tenancy;
|
|
64
|
+
if (parts.surfaces !== undefined)
|
|
65
|
+
product.surfaces = parts.surfaces;
|
|
66
|
+
return Object.keys(product).length > 0 ? product : undefined;
|
|
67
|
+
}
|
|
68
|
+
function readSurfaces(node) {
|
|
69
|
+
if (!isSeq(node)) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
issues: [
|
|
73
|
+
issue("unknown-field", 'Field "product.surfaces" must be a list of ids.', "product.surfaces"),
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (node.items.length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
issues: [
|
|
81
|
+
issue("unknown-field", 'Field "product.surfaces" must not be empty. Omit the key instead.', "product.surfaces"),
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const issues = [];
|
|
86
|
+
const surfaces = [];
|
|
87
|
+
const ids = new Set();
|
|
88
|
+
for (const entry of node.items) {
|
|
89
|
+
if (!isMap(entry)) {
|
|
90
|
+
issues.push(issue("unknown-field", "Each surface must be a mapping with an id.", "product.surfaces"));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let id;
|
|
94
|
+
let sawId = false;
|
|
95
|
+
for (const pair of entry.items) {
|
|
96
|
+
const key = mappingKey(pair.key);
|
|
97
|
+
if (key !== "id") {
|
|
98
|
+
issues.push(issue("unknown-field", `Unknown field "product.surfaces.${key ?? ""}".`, "product.surfaces"));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (sawId) {
|
|
102
|
+
issues.push(issue("duplicate-key", 'Duplicate key "id".', "product.surfaces"));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
sawId = true;
|
|
106
|
+
id = isScalar(pair.value) ? pair.value.value : NON_SCALAR;
|
|
107
|
+
}
|
|
108
|
+
if (!sawId) {
|
|
109
|
+
issues.push(issue("missing-field", "Missing surface id.", "product.surfaces"));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (typeof id !== "string") {
|
|
113
|
+
issues.push(issue("unknown-field", "Surface id must be a string.", "product.surfaces"));
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!SURFACE_ID.test(id)) {
|
|
117
|
+
issues.push(issue("unknown-field", `Surface id ${JSON.stringify(id)} must match ^[a-z][a-z0-9-]*$.`, "product.surfaces"));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (ids.has(id)) {
|
|
121
|
+
issues.push(issue("duplicate-key", `Duplicate surface id "${id}".`, "product.surfaces"));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
ids.add(id);
|
|
125
|
+
surfaces.push({ id });
|
|
126
|
+
}
|
|
127
|
+
return issues.length > 0 ? { ok: false, issues } : { ok: true, surfaces };
|
|
128
|
+
}
|
|
129
|
+
function readProduct(node) {
|
|
130
|
+
if (!isMap(node)) {
|
|
131
|
+
return { issues: [issue("unknown-field", 'Unknown field "product".', "product")] };
|
|
132
|
+
}
|
|
133
|
+
const issues = [];
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
let screenLanguage;
|
|
136
|
+
let tenancy;
|
|
137
|
+
let surfaces;
|
|
138
|
+
for (const item of node.items) {
|
|
139
|
+
const key = mappingKey(item.key);
|
|
140
|
+
if (key === null) {
|
|
141
|
+
issues.push(issue("unknown-field", "Mapping keys must be strings.", "product"));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (seen.has(key)) {
|
|
145
|
+
issues.push(issue("duplicate-key", `Duplicate key "${key}".`, `product.${key}`));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
seen.add(key);
|
|
149
|
+
if (!PRODUCT_FIELDS.has(key)) {
|
|
150
|
+
issues.push(issue("unknown-field", `Unknown field "product.${key}".`, `product.${key}`));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (key === "screenLanguage") {
|
|
154
|
+
const value = isScalar(item.value) ? item.value.value : null;
|
|
155
|
+
if (!isScreenLanguage(value)) {
|
|
156
|
+
issues.push(issue("unknown-field", 'Unknown field "product.screenLanguage".', "product.screenLanguage"));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
screenLanguage = value;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (key === "tenancy") {
|
|
164
|
+
const value = isScalar(item.value) ? item.value.value : null;
|
|
165
|
+
if (!isTenancy(value)) {
|
|
166
|
+
issues.push(issue("unknown-field", `Unknown tenancy ${shown(value)}.`, "product.tenancy"));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
tenancy = value;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const parsed = readSurfaces(item.value);
|
|
174
|
+
if (!parsed.ok) {
|
|
175
|
+
issues.push(...parsed.issues);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
surfaces = parsed.surfaces;
|
|
179
|
+
}
|
|
180
|
+
if (issues.length > 0)
|
|
181
|
+
return { issues };
|
|
182
|
+
const product = assembleProduct({ screenLanguage, tenancy, surfaces });
|
|
183
|
+
return product ? { product, issues } : { issues };
|
|
184
|
+
}
|
|
28
185
|
function profileIssue(value) {
|
|
29
186
|
if (typeof value !== "string") {
|
|
30
187
|
return issue("unknown-profile", "Profile must be a string.", "profile");
|
|
@@ -65,6 +222,7 @@ export function parseConfig(text) {
|
|
|
65
222
|
const issues = [];
|
|
66
223
|
const occurrences = new Map();
|
|
67
224
|
const values = new Map();
|
|
225
|
+
let product;
|
|
68
226
|
for (const item of document.contents.items) {
|
|
69
227
|
const key = isScalar(item.key) && typeof item.key.value === "string" ? item.key.value : null;
|
|
70
228
|
if (key === null) {
|
|
@@ -81,6 +239,23 @@ export function parseConfig(text) {
|
|
|
81
239
|
issues.push(issue("unknown-field", `Unknown field "${key}".`, key));
|
|
82
240
|
continue;
|
|
83
241
|
}
|
|
242
|
+
if (key === "git") {
|
|
243
|
+
const base = readGitBase(item.value);
|
|
244
|
+
if (base === "invalid") {
|
|
245
|
+
issues.push(issue("unknown-field", 'Unknown field "git.defaultBase".', "git.defaultBase"));
|
|
246
|
+
}
|
|
247
|
+
else if (base) {
|
|
248
|
+
values.set("git.defaultBase", base);
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (key === "product") {
|
|
253
|
+
const parsed = readProduct(item.value);
|
|
254
|
+
issues.push(...parsed.issues);
|
|
255
|
+
if (parsed.product)
|
|
256
|
+
product = parsed.product;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
84
259
|
values.set(key, isScalar(item.value) ? item.value.value : NON_SCALAR);
|
|
85
260
|
}
|
|
86
261
|
if ((occurrences.get("schemaVersion") ?? 0) === 0) {
|
|
@@ -104,5 +279,13 @@ export function parseConfig(text) {
|
|
|
104
279
|
issue("unknown-profile", "Profile must be a string.", "profile"),
|
|
105
280
|
]);
|
|
106
281
|
}
|
|
282
|
+
const defaultBase = values.get("git.defaultBase");
|
|
283
|
+
const git = isBranchBase(defaultBase) ? { defaultBase } : undefined;
|
|
284
|
+
if (git && product)
|
|
285
|
+
return { schemaVersion: CONFIG_SCHEMA_VERSION, profile, git, product };
|
|
286
|
+
if (git)
|
|
287
|
+
return { schemaVersion: CONFIG_SCHEMA_VERSION, profile, git };
|
|
288
|
+
if (product)
|
|
289
|
+
return { schemaVersion: CONFIG_SCHEMA_VERSION, profile, product };
|
|
107
290
|
return { schemaVersion: CONFIG_SCHEMA_VERSION, profile };
|
|
108
291
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4
|
+
const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
5
|
+
const OUTCOME_LINE = /^outcome:[ \t]*(\S+)[ \t]*$/m;
|
|
6
|
+
export const FEEDBACK_OUTCOMES = ["accepted", "edited", "expanded"];
|
|
7
|
+
export class FeedbackError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "FeedbackError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function errno(error) {
|
|
16
|
+
if (typeof error === "object" &&
|
|
17
|
+
error !== null &&
|
|
18
|
+
"code" in error &&
|
|
19
|
+
typeof error.code === "string") {
|
|
20
|
+
return error.code;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function isFeedbackOutcome(value) {
|
|
25
|
+
return typeof value === "string" && FEEDBACK_OUTCOMES.includes(value);
|
|
26
|
+
}
|
|
27
|
+
export function readFeedbackOutcome(reviewText) {
|
|
28
|
+
const frontMatter = FRONT_MATTER.exec(reviewText)?.[1];
|
|
29
|
+
if (frontMatter === undefined) {
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
const value = OUTCOME_LINE.exec(frontMatter.replace(/\r/g, ""))?.[1];
|
|
33
|
+
return isFeedbackOutcome(value) ? value : "unknown";
|
|
34
|
+
}
|
|
35
|
+
export function saveFeedback(root, slug, record) {
|
|
36
|
+
if (!SLUG.test(slug)) {
|
|
37
|
+
throw new FeedbackError("invalid-slug", "Feedback slug must be a lowercase hyphenated name.");
|
|
38
|
+
}
|
|
39
|
+
if (!isFeedbackOutcome(record.outcome)) {
|
|
40
|
+
throw new FeedbackError("invalid-outcome", `Feedback outcome must be one of: ${FEEDBACK_OUTCOMES.join(", ")}.`);
|
|
41
|
+
}
|
|
42
|
+
const dir = join(root, ".tito", "feedback", slug);
|
|
43
|
+
const planPath = join(dir, "plan.md");
|
|
44
|
+
const reviewPath = join(dir, "review.md");
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(planPath, record.plan, { encoding: "utf8", flag: "wx" });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (errno(error) === "EEXIST") {
|
|
51
|
+
throw new FeedbackError("already-saved", "The Tito plan is already saved.");
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(reviewPath, `---\noutcome: ${record.outcome}\n---\n${record.review}`, {
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
flag: "wx",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (errno(error) === "EEXIST") {
|
|
63
|
+
throw new FeedbackError("already-saved", "The review is already saved.");
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
return { planPath, reviewPath };
|
|
68
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export const BRANCH_TYPES = [
|
|
2
|
+
"feat",
|
|
3
|
+
"fix",
|
|
4
|
+
"hot-fix",
|
|
5
|
+
"chores",
|
|
6
|
+
"refactor",
|
|
7
|
+
"debug",
|
|
8
|
+
];
|
|
9
|
+
export const BRANCH_BASES = ["dev", "develop", "main", "master"];
|
|
10
|
+
const TYPE_BY_KIND = {
|
|
11
|
+
feature: "feat",
|
|
12
|
+
bug: "fix",
|
|
13
|
+
"production-emergency": "hot-fix",
|
|
14
|
+
tooling: "chores",
|
|
15
|
+
refactor: "refactor",
|
|
16
|
+
investigation: "debug",
|
|
17
|
+
};
|
|
18
|
+
export function branchSlug(text) {
|
|
19
|
+
const slug = text
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
22
|
+
.replace(/^-+|-+$/g, "")
|
|
23
|
+
.slice(0, 40)
|
|
24
|
+
.replace(/-+$/g, "");
|
|
25
|
+
return slug.length > 0 ? slug : "change";
|
|
26
|
+
}
|
|
27
|
+
function isBranchBase(value) {
|
|
28
|
+
return BRANCH_BASES.includes(value);
|
|
29
|
+
}
|
|
30
|
+
function firstExisting(names, existing, fallback) {
|
|
31
|
+
return names.find((name) => existing.has(name)) ?? fallback;
|
|
32
|
+
}
|
|
33
|
+
export function suggestBranch(input) {
|
|
34
|
+
const type = TYPE_BY_KIND[input.taskKind];
|
|
35
|
+
const existing = new Set(input.existingBases);
|
|
36
|
+
const current = input.currentBranch;
|
|
37
|
+
let base;
|
|
38
|
+
let reason;
|
|
39
|
+
if (type === "hot-fix") {
|
|
40
|
+
base =
|
|
41
|
+
input.defaultBase === "main" || input.defaultBase === "master"
|
|
42
|
+
? input.defaultBase
|
|
43
|
+
: firstExisting(["main", "master"], existing, "main");
|
|
44
|
+
reason = `A production emergency starts from ${base}.`;
|
|
45
|
+
}
|
|
46
|
+
else if (input.continuesFromCurrent && isBranchBase(current)) {
|
|
47
|
+
base = current;
|
|
48
|
+
reason = `The work continues from ${current}.`;
|
|
49
|
+
}
|
|
50
|
+
else if (input.defaultBase) {
|
|
51
|
+
base = input.defaultBase;
|
|
52
|
+
reason = `The project default base is ${input.defaultBase}.`;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
base = firstExisting(BRANCH_BASES, existing, "main");
|
|
56
|
+
reason =
|
|
57
|
+
base === "dev" || base === "develop"
|
|
58
|
+
? `Feature work starts from ${base}.`
|
|
59
|
+
: "This repository has no dev or develop branch.";
|
|
60
|
+
}
|
|
61
|
+
const name = `${type}/${branchSlug(input.slug)}`;
|
|
62
|
+
return {
|
|
63
|
+
type,
|
|
64
|
+
base,
|
|
65
|
+
name,
|
|
66
|
+
reason,
|
|
67
|
+
line: `Suggested branch: ${name} from ${base}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function checkoutCommands(base, branch) {
|
|
71
|
+
return [`git checkout ${base}`, `git checkout -b ${branch}`];
|
|
72
|
+
}
|
|
73
|
+
export function prunePlanIndex(entries, planExists) {
|
|
74
|
+
return entries.filter((entry) => planExists(entry.cursorPlanPath));
|
|
75
|
+
}
|
|
76
|
+
export function linearReference(input) {
|
|
77
|
+
if (input.issueId && input.issueId.trim().length > 0) {
|
|
78
|
+
return { issueId: input.issueId.trim(), createIssue: false };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
issueId: null,
|
|
82
|
+
createIssue: input.userAskedToCreate,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export const PULL_REQUEST_TEMPLATE_PATH = ".github/pull_request_template.md";
|
|
86
|
+
const PULL_REQUEST_HEADINGS = {
|
|
87
|
+
problem: "## Problem",
|
|
88
|
+
done: "## What changed",
|
|
89
|
+
review: "## Review fixes",
|
|
90
|
+
checks: "## Checks",
|
|
91
|
+
};
|
|
92
|
+
const MAX_PULL_REQUEST_LINES = 50;
|
|
93
|
+
export function pullRequestTemplate() {
|
|
94
|
+
return [
|
|
95
|
+
PULL_REQUEST_HEADINGS.problem,
|
|
96
|
+
"",
|
|
97
|
+
PULL_REQUEST_HEADINGS.done,
|
|
98
|
+
"",
|
|
99
|
+
"-",
|
|
100
|
+
"",
|
|
101
|
+
PULL_REQUEST_HEADINGS.review,
|
|
102
|
+
"",
|
|
103
|
+
"-",
|
|
104
|
+
"",
|
|
105
|
+
PULL_REQUEST_HEADINGS.checks,
|
|
106
|
+
"",
|
|
107
|
+
"- Lint:",
|
|
108
|
+
"- Code quality:",
|
|
109
|
+
"- Conventions:",
|
|
110
|
+
"- Tests:",
|
|
111
|
+
"- Build:",
|
|
112
|
+
"- Docs:",
|
|
113
|
+
"",
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
function bullet(item) {
|
|
117
|
+
const text = item.trim();
|
|
118
|
+
if (text.length === 0 || text.includes("\n"))
|
|
119
|
+
return null;
|
|
120
|
+
return `- ${text}`;
|
|
121
|
+
}
|
|
122
|
+
function bullets(items) {
|
|
123
|
+
const lines = [];
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
const line = bullet(item);
|
|
126
|
+
if (line === null)
|
|
127
|
+
return null;
|
|
128
|
+
lines.push(line);
|
|
129
|
+
}
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
export function pullRequestDescription(input) {
|
|
133
|
+
const problem = input.problem.trim();
|
|
134
|
+
if (problem.length === 0 || problem.includes("\n"))
|
|
135
|
+
return null;
|
|
136
|
+
const done = bullets(input.done);
|
|
137
|
+
const checks = bullets(input.checks);
|
|
138
|
+
if (done === null || checks === null || done.length === 0 || checks.length === 0)
|
|
139
|
+
return null;
|
|
140
|
+
const review = bullets(input.reviewFixes.length > 0 ? input.reviewFixes : ["No review fixes."]);
|
|
141
|
+
if (review === null)
|
|
142
|
+
return null;
|
|
143
|
+
const lines = [
|
|
144
|
+
PULL_REQUEST_HEADINGS.problem,
|
|
145
|
+
problem,
|
|
146
|
+
"",
|
|
147
|
+
PULL_REQUEST_HEADINGS.done,
|
|
148
|
+
...done,
|
|
149
|
+
"",
|
|
150
|
+
PULL_REQUEST_HEADINGS.review,
|
|
151
|
+
...review,
|
|
152
|
+
"",
|
|
153
|
+
PULL_REQUEST_HEADINGS.checks,
|
|
154
|
+
...checks,
|
|
155
|
+
];
|
|
156
|
+
if (lines.length < 2 || lines.length > MAX_PULL_REQUEST_LINES)
|
|
157
|
+
return null;
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
export function mergeDecision(input) {
|
|
161
|
+
if (!input.userAskedToMerge) {
|
|
162
|
+
return { allowed: false, reason: "Wait for the user to call for the merge." };
|
|
163
|
+
}
|
|
164
|
+
if (!input.hasApproval) {
|
|
165
|
+
return { allowed: false, reason: "The pull request has no approval." };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
allowed: true,
|
|
169
|
+
reason: "The user called for the merge and the pull request has an approval.",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function canApprovePullRequest() {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
package/dist/core/init.js
CHANGED
|
@@ -2,8 +2,9 @@ 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 { PULL_REQUEST_TEMPLATE_PATH, pullRequestTemplate } from "./git-flow.js";
|
|
5
6
|
import { ACTIVE_RISK_PROFILE_IDS } from "./profiles.js";
|
|
6
|
-
import {
|
|
7
|
+
import { PlanError, TITO_BOOTSTRAP_START, planAdoption, titoBootstrapBlock, } from "./plan.js";
|
|
7
8
|
export class InitError extends Error {
|
|
8
9
|
code;
|
|
9
10
|
constructor(message) {
|
|
@@ -31,7 +32,7 @@ function exists(root, path) {
|
|
|
31
32
|
return false;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
function shippedSkills() {
|
|
35
|
+
export function shippedSkills() {
|
|
35
36
|
const root = join(dirname(fileURLToPath(import.meta.url)), "../../templates/cursor/skills");
|
|
36
37
|
return readdirSync(root, { withFileTypes: true })
|
|
37
38
|
.filter((entry) => entry.isDirectory())
|
|
@@ -42,16 +43,17 @@ function shippedSkills() {
|
|
|
42
43
|
}
|
|
43
44
|
function agentsBootstrap(report) {
|
|
44
45
|
if (report.agentsMd === "absent") {
|
|
45
|
-
return { path: "AGENTS.md", action: "create", body:
|
|
46
|
+
return { path: "AGENTS.md", action: "create", body: titoBootstrapBlock() };
|
|
46
47
|
}
|
|
47
48
|
const current = readFileSync(join(report.root, "AGENTS.md"), "utf8");
|
|
48
|
-
if (current.includes(
|
|
49
|
+
if (current.includes(TITO_BOOTSTRAP_START) ||
|
|
50
|
+
current.includes("Hola, Tito here!")) {
|
|
49
51
|
return { path: "AGENTS.md", action: "keep" };
|
|
50
52
|
}
|
|
51
53
|
return {
|
|
52
54
|
path: "AGENTS.md",
|
|
53
55
|
action: "append",
|
|
54
|
-
body: `\n${
|
|
56
|
+
body: `\n${titoBootstrapBlock()}`,
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
export function planInitialization(report, profile) {
|
|
@@ -63,7 +65,15 @@ export function planInitialization(report, profile) {
|
|
|
63
65
|
const skills = shippedSkills().map((skill) => exists(report.root, skill.path)
|
|
64
66
|
? { path: skill.path, action: "keep" }
|
|
65
67
|
: { path: skill.path, action: "create", body: skill.body });
|
|
66
|
-
|
|
68
|
+
const template = pullRequestTemplate();
|
|
69
|
+
const pullRequest = exists(report.root, PULL_REQUEST_TEMPLATE_PATH)
|
|
70
|
+
? { path: PULL_REQUEST_TEMPLATE_PATH, action: "keep" }
|
|
71
|
+
: { path: PULL_REQUEST_TEMPLATE_PATH, action: "create", body: template };
|
|
72
|
+
return {
|
|
73
|
+
root: adoption.root,
|
|
74
|
+
profile: adoption.profile,
|
|
75
|
+
files: [...files, ...agents, ...skills, pullRequest],
|
|
76
|
+
};
|
|
67
77
|
}
|
|
68
78
|
export function formatInitialization(plan) {
|
|
69
79
|
const lines = [`root: ${plan.root}`, `profile: ${plan.profile}`];
|
package/dist/core/plan.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ACTIVE_RISK_PROFILE_IDS, isActiveRiskProfileId, isFutureRiskProfileId, } from "./profiles.js";
|
|
2
|
+
export const TITO_BOOTSTRAP_START = "<!-- tito:bootstrap start -->";
|
|
3
|
+
export const TITO_BOOTSTRAP_END = "<!-- tito:bootstrap end -->";
|
|
2
4
|
export const ADOPTION_AGENTS_MD = `# Tito bootstrap
|
|
3
5
|
|
|
4
6
|
This project uses Tito as its root engineering coordinator.
|
|
@@ -12,9 +14,13 @@ This project uses Tito as its root engineering coordinator.
|
|
|
12
14
|
- Never commit, push, publish, deploy, or perform irreversible external actions without explicit approval.
|
|
13
15
|
- Use \`/tito\` when Tito coordination is wanted.
|
|
14
16
|
- Plans own technical decisions and include guidance code when useful.
|
|
17
|
+
- Confirm intent before clarifying. Plans include decisions with conventions, AAA test cases, and docs impact.
|
|
15
18
|
|
|
16
19
|
Keep this bootstrap compact. Load project documentation only when the task needs it.
|
|
17
20
|
`;
|
|
21
|
+
export function titoBootstrapBlock() {
|
|
22
|
+
return `${TITO_BOOTSTRAP_START}\n${ADOPTION_AGENTS_MD.trim()}\n${TITO_BOOTSTRAP_END}\n`;
|
|
23
|
+
}
|
|
18
24
|
export class PlanError extends Error {
|
|
19
25
|
code;
|
|
20
26
|
constructor(code, message) {
|
package/dist/core/profiles.js
CHANGED
|
@@ -27,8 +27,8 @@ export const RISK_PROFILES = deepFreeze({
|
|
|
27
27
|
"client-careful": {
|
|
28
28
|
id: "client-careful",
|
|
29
29
|
status: "active",
|
|
30
|
-
maxWriters:
|
|
31
|
-
writerConstraint: "
|
|
30
|
+
maxWriters: 3,
|
|
31
|
+
writerConstraint: "bounded-parallel",
|
|
32
32
|
independentReview: "required",
|
|
33
33
|
preferredAuthoredLines: { min: 100, max: 200 },
|
|
34
34
|
maxAuthoredLinesWithoutApproval: 300,
|
|
@@ -49,8 +49,8 @@ export const RISK_PROFILES = deepFreeze({
|
|
|
49
49
|
"solo-balanced": {
|
|
50
50
|
id: "solo-balanced",
|
|
51
51
|
status: "active",
|
|
52
|
-
maxWriters:
|
|
53
|
-
writerConstraint: "
|
|
52
|
+
maxWriters: 6,
|
|
53
|
+
writerConstraint: "bounded-parallel",
|
|
54
54
|
humanReview: "feature-boundaries",
|
|
55
55
|
independentReviewFor: ["security", "data", "migrations", "architecture"],
|
|
56
56
|
diffLimits: "moderate",
|
|
@@ -60,8 +60,8 @@ export const RISK_PROFILES = deepFreeze({
|
|
|
60
60
|
"solo-fast": {
|
|
61
61
|
id: "solo-fast",
|
|
62
62
|
status: "active",
|
|
63
|
-
maxWriters:
|
|
64
|
-
writerConstraint: "
|
|
63
|
+
maxWriters: 12,
|
|
64
|
+
writerConstraint: "bounded-parallel",
|
|
65
65
|
agentSelfReview: "allowed",
|
|
66
66
|
humanReview: "milestones",
|
|
67
67
|
safetyFloor: MANDATORY_ESCALATION_TOPICS,
|