@anhvupt/tito 0.1.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/LICENSE +201 -0
- package/README.md +170 -0
- package/dist/cli.js +208 -0
- package/dist/core/agents.js +66 -0
- package/dist/core/config.js +108 -0
- package/dist/core/init.js +65 -0
- package/dist/core/inspect.js +89 -0
- package/dist/core/lifecycle.js +54 -0
- package/dist/core/plan.js +59 -0
- package/dist/core/profiles.js +81 -0
- package/dist/core/routing.js +35 -0
- package/dist/core/specialists.js +237 -0
- package/dist/core/work-plan.js +270 -0
- package/package.json +32 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { isMap, isScalar, parseAllDocuments } from "yaml";
|
|
2
|
+
import { ACTIVE_RISK_PROFILE_IDS, isActiveRiskProfileId, isFutureRiskProfileId, } from "./profiles.js";
|
|
3
|
+
export const CONFIG_SCHEMA_VERSION = 1;
|
|
4
|
+
const FIELDS = new Set(["schemaVersion", "profile"]);
|
|
5
|
+
const NON_SCALAR = Symbol("non-scalar");
|
|
6
|
+
export class ConfigValidationError extends Error {
|
|
7
|
+
issues;
|
|
8
|
+
constructor(issues) {
|
|
9
|
+
super(issues.map((item) => item.message).join("\n"));
|
|
10
|
+
this.name = "ConfigValidationError";
|
|
11
|
+
this.issues = issues;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function issue(code, message, path) {
|
|
15
|
+
return path === undefined ? { code, message } : { code, message, path };
|
|
16
|
+
}
|
|
17
|
+
function shown(value) {
|
|
18
|
+
if (typeof value === "string")
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
if (typeof value === "number" ||
|
|
21
|
+
typeof value === "boolean" ||
|
|
22
|
+
typeof value === "bigint" ||
|
|
23
|
+
value === null) {
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
return "a non-scalar value";
|
|
27
|
+
}
|
|
28
|
+
function profileIssue(value) {
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
return issue("unknown-profile", "Profile must be a string.", "profile");
|
|
31
|
+
}
|
|
32
|
+
if (isFutureRiskProfileId(value)) {
|
|
33
|
+
return issue("unsupported-profile", `Profile "${value}" is recognized but not available.`, "profile");
|
|
34
|
+
}
|
|
35
|
+
if (isActiveRiskProfileId(value))
|
|
36
|
+
return null;
|
|
37
|
+
return issue("unknown-profile", `Unknown profile "${value}". Active profiles: ${ACTIVE_RISK_PROFILE_IDS.join(", ")}.`, "profile");
|
|
38
|
+
}
|
|
39
|
+
export function parseConfig(text) {
|
|
40
|
+
const documents = parseAllDocuments(text, {
|
|
41
|
+
logLevel: "silent",
|
|
42
|
+
merge: false,
|
|
43
|
+
stringKeys: true,
|
|
44
|
+
uniqueKeys: true,
|
|
45
|
+
version: "1.2",
|
|
46
|
+
});
|
|
47
|
+
const document = documents[0];
|
|
48
|
+
if (documents.length !== 1 || !document) {
|
|
49
|
+
const multiple = documents.length > 1;
|
|
50
|
+
throw new ConfigValidationError([
|
|
51
|
+
issue(multiple ? "yaml-syntax" : "not-a-mapping", multiple
|
|
52
|
+
? "Multiple YAML documents are not allowed."
|
|
53
|
+
: "Configuration must be a mapping."),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
const syntax = document.errors.filter((error) => error.code !== "DUPLICATE_KEY" && error.code !== "NON_STRING_KEY");
|
|
57
|
+
if (syntax.length > 0) {
|
|
58
|
+
throw new ConfigValidationError(syntax.map((error) => issue("yaml-syntax", `YAML syntax error (${error.code}).`)));
|
|
59
|
+
}
|
|
60
|
+
if (!isMap(document.contents)) {
|
|
61
|
+
throw new ConfigValidationError([
|
|
62
|
+
issue("not-a-mapping", "Configuration must be a mapping."),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
const issues = [];
|
|
66
|
+
const occurrences = new Map();
|
|
67
|
+
const values = new Map();
|
|
68
|
+
for (const item of document.contents.items) {
|
|
69
|
+
const key = isScalar(item.key) && typeof item.key.value === "string" ? item.key.value : null;
|
|
70
|
+
if (key === null) {
|
|
71
|
+
issues.push(issue("unknown-field", "Mapping keys must be strings."));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const count = (occurrences.get(key) ?? 0) + 1;
|
|
75
|
+
occurrences.set(key, count);
|
|
76
|
+
if (count > 1) {
|
|
77
|
+
issues.push(issue("duplicate-key", `Duplicate key "${key}".`, key));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!FIELDS.has(key)) {
|
|
81
|
+
issues.push(issue("unknown-field", `Unknown field "${key}".`, key));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
values.set(key, isScalar(item.value) ? item.value.value : NON_SCALAR);
|
|
85
|
+
}
|
|
86
|
+
if ((occurrences.get("schemaVersion") ?? 0) === 0) {
|
|
87
|
+
issues.push(issue("missing-field", 'Missing required field "schemaVersion".', "schemaVersion"));
|
|
88
|
+
}
|
|
89
|
+
else if (occurrences.get("schemaVersion") === 1 && values.get("schemaVersion") !== CONFIG_SCHEMA_VERSION) {
|
|
90
|
+
issues.push(issue("unsupported-schema-version", `Unsupported schemaVersion ${shown(values.get("schemaVersion"))}. Expected ${CONFIG_SCHEMA_VERSION}.`, "schemaVersion"));
|
|
91
|
+
}
|
|
92
|
+
const profileError = (occurrences.get("profile") ?? 0) === 0
|
|
93
|
+
? issue("missing-field", 'Missing required field "profile".', "profile")
|
|
94
|
+
: occurrences.get("profile") === 1
|
|
95
|
+
? profileIssue(values.get("profile"))
|
|
96
|
+
: null;
|
|
97
|
+
if (profileError)
|
|
98
|
+
issues.push(profileError);
|
|
99
|
+
if (issues.length > 0)
|
|
100
|
+
throw new ConfigValidationError(issues);
|
|
101
|
+
const profile = values.get("profile");
|
|
102
|
+
if (typeof profile !== "string" || !isActiveRiskProfileId(profile)) {
|
|
103
|
+
throw new ConfigValidationError([
|
|
104
|
+
issue("unknown-profile", "Profile must be a string.", "profile"),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
return { schemaVersion: CONFIG_SCHEMA_VERSION, profile };
|
|
108
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { compiledCursorAgents } from "./agents.js";
|
|
4
|
+
import { PlanError, planAdoption, } from "./plan.js";
|
|
5
|
+
export class InitError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "InitError";
|
|
10
|
+
this.code = "conflict";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function exists(root, path) {
|
|
14
|
+
try {
|
|
15
|
+
return statSync(join(root, path)).isFile();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function planInitialization(report, profile) {
|
|
22
|
+
const adoption = planAdoption(report, profile);
|
|
23
|
+
const agents = compiledCursorAgents().map((agent) => exists(report.root, agent.path)
|
|
24
|
+
? { path: agent.path, action: "conflict" }
|
|
25
|
+
: { path: agent.path, action: "create", body: agent.body });
|
|
26
|
+
return { root: adoption.root, profile: adoption.profile, files: [...adoption.files, ...agents] };
|
|
27
|
+
}
|
|
28
|
+
export function formatInitialization(plan) {
|
|
29
|
+
const lines = [`root: ${plan.root}`, `profile: ${plan.profile}`];
|
|
30
|
+
for (const file of plan.files) {
|
|
31
|
+
lines.push(`${file.path}: ${file.action}`);
|
|
32
|
+
if (file.action === "create")
|
|
33
|
+
lines.push("---", file.body.trimEnd(), "---");
|
|
34
|
+
}
|
|
35
|
+
return `${lines.join("\n")}\n`;
|
|
36
|
+
}
|
|
37
|
+
export function applyInitialization(plan) {
|
|
38
|
+
const conflicts = plan.files.filter((file) => file.action === "conflict");
|
|
39
|
+
if (conflicts.length > 0) {
|
|
40
|
+
throw new InitError(`Refusing to write because ${conflicts.map((file) => file.path).join(", ")} already exists.`);
|
|
41
|
+
}
|
|
42
|
+
const created = [];
|
|
43
|
+
try {
|
|
44
|
+
for (const file of plan.files) {
|
|
45
|
+
if (file.action !== "create")
|
|
46
|
+
continue;
|
|
47
|
+
const absolute = join(plan.root, file.path);
|
|
48
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
49
|
+
writeFileSync(absolute, file.body, { encoding: "utf8", flag: "wx" });
|
|
50
|
+
created.push(absolute);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
for (const absolute of created) {
|
|
55
|
+
try {
|
|
56
|
+
unlinkSync(absolute);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Keep the original write error.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export { PlanError };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { ConfigValidationError, parseConfig, } from "./config.js";
|
|
4
|
+
export class InspectionError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
path;
|
|
7
|
+
constructor(code, path, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "InspectionError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function errno(error) {
|
|
15
|
+
if (typeof error === "object" &&
|
|
16
|
+
error !== null &&
|
|
17
|
+
"code" in error &&
|
|
18
|
+
typeof error.code === "string") {
|
|
19
|
+
return error.code;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function fail(code, path, message) {
|
|
24
|
+
throw new InspectionError(code, path, message);
|
|
25
|
+
}
|
|
26
|
+
function fileKind(path) {
|
|
27
|
+
try {
|
|
28
|
+
if (statSync(path).isFile())
|
|
29
|
+
return "file";
|
|
30
|
+
fail("filesystem", path, `${path} must be a file.`);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof InspectionError)
|
|
34
|
+
throw error;
|
|
35
|
+
const code = errno(error);
|
|
36
|
+
if (code === "ENOENT")
|
|
37
|
+
return "absent";
|
|
38
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
39
|
+
fail("inaccessible", path, `Cannot read ${path}.`);
|
|
40
|
+
}
|
|
41
|
+
fail("filesystem", path, `Cannot inspect ${path}.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function readConfig(path) {
|
|
45
|
+
if (fileKind(path) === "absent")
|
|
46
|
+
return { status: "absent" };
|
|
47
|
+
try {
|
|
48
|
+
return { status: "valid", config: parseConfig(readFileSync(path, "utf8")) };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof ConfigValidationError) {
|
|
52
|
+
return { status: "invalid", issues: error.issues };
|
|
53
|
+
}
|
|
54
|
+
fail("filesystem", path, `Cannot read ${path}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function inspectRepository(root) {
|
|
58
|
+
const resolved = resolve(root);
|
|
59
|
+
try {
|
|
60
|
+
if (!statSync(resolved).isDirectory()) {
|
|
61
|
+
fail("not-a-directory", resolved, `${resolved} is not a directory.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof InspectionError)
|
|
66
|
+
throw error;
|
|
67
|
+
const code = errno(error);
|
|
68
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
69
|
+
fail("inaccessible", resolved, `Cannot read ${resolved}.`);
|
|
70
|
+
}
|
|
71
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
72
|
+
fail("not-a-directory", resolved, `${resolved} is not a directory.`);
|
|
73
|
+
}
|
|
74
|
+
fail("filesystem", resolved, `Cannot inspect ${resolved}.`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
root: resolved,
|
|
78
|
+
titoYaml: readConfig(join(resolved, "tito.yaml")),
|
|
79
|
+
agentsMd: fileKind(join(resolved, "AGENTS.md")) === "file" ? "present" : "absent",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function formatInspection(report) {
|
|
83
|
+
const config = report.titoYaml.status === "absent"
|
|
84
|
+
? "absent"
|
|
85
|
+
: report.titoYaml.status === "valid"
|
|
86
|
+
? `valid profile=${report.titoYaml.config.profile}`
|
|
87
|
+
: `invalid ${report.titoYaml.issues.map((item) => item.code).join(",")}`;
|
|
88
|
+
return `root: ${report.root}\ntito.yaml: ${config}\nAGENTS.md: ${report.agentsMd}\n`;
|
|
89
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const LIFECYCLE_STATES = Object.freeze([
|
|
2
|
+
"DISCOVERY",
|
|
3
|
+
"PLANNED",
|
|
4
|
+
"APPROVED",
|
|
5
|
+
"IMPLEMENTING",
|
|
6
|
+
"READY_FOR_REVIEW",
|
|
7
|
+
"APPROVED_FOR_COMMIT",
|
|
8
|
+
"DONE",
|
|
9
|
+
]);
|
|
10
|
+
const LINEAR_NEXT = {
|
|
11
|
+
DISCOVERY: "PLANNED",
|
|
12
|
+
PLANNED: "APPROVED",
|
|
13
|
+
APPROVED: "IMPLEMENTING",
|
|
14
|
+
IMPLEMENTING: "READY_FOR_REVIEW",
|
|
15
|
+
READY_FOR_REVIEW: "APPROVED_FOR_COMMIT",
|
|
16
|
+
APPROVED_FOR_COMMIT: "DONE",
|
|
17
|
+
DONE: null,
|
|
18
|
+
};
|
|
19
|
+
export class LifecycleTransitionError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
from;
|
|
22
|
+
to;
|
|
23
|
+
constructor(code, from, to, message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "LifecycleTransitionError";
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.from = from;
|
|
28
|
+
this.to = to;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isLifecycleState(value) {
|
|
32
|
+
return LIFECYCLE_STATES.includes(value);
|
|
33
|
+
}
|
|
34
|
+
function reject(code, from, to, message) {
|
|
35
|
+
throw new LifecycleTransitionError(code, from, to, message);
|
|
36
|
+
}
|
|
37
|
+
export function transitionLifecycle(from, to, authorization) {
|
|
38
|
+
if (!isLifecycleState(from) || !isLifecycleState(to)) {
|
|
39
|
+
reject("invalid-state", from, to, "Lifecycle transition requires known states.");
|
|
40
|
+
}
|
|
41
|
+
if (LINEAR_NEXT[from] === to)
|
|
42
|
+
return to;
|
|
43
|
+
if (from === "DISCOVERY" && to === "APPROVED") {
|
|
44
|
+
if (authorization.approvedSmallSlice)
|
|
45
|
+
return to;
|
|
46
|
+
reject("missing-authorization", from, to, "DISCOVERY can move to APPROVED only for an explicitly approved small slice.");
|
|
47
|
+
}
|
|
48
|
+
if (from === "READY_FOR_REVIEW" && to === "IMPLEMENTING") {
|
|
49
|
+
if (authorization.inScopeReviewRevision)
|
|
50
|
+
return to;
|
|
51
|
+
reject("missing-authorization", from, to, "READY_FOR_REVIEW can return to IMPLEMENTING only for an in-scope review revision.");
|
|
52
|
+
}
|
|
53
|
+
reject("illegal-transition", from, to, `Cannot transition from ${from} to ${to}.`);
|
|
54
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ACTIVE_RISK_PROFILE_IDS, isActiveRiskProfileId, isFutureRiskProfileId, } from "./profiles.js";
|
|
2
|
+
export const ADOPTION_AGENTS_MD = `# Tito bootstrap
|
|
3
|
+
|
|
4
|
+
This project uses Tito as its root engineering coordinator.
|
|
5
|
+
|
|
6
|
+
- Start every Tito-coordinated chat response exactly with \`Hola, Tito here!\`
|
|
7
|
+
- Keep that marker out of CLI and machine-readable output.
|
|
8
|
+
- Treat ordinary requests as Tito-coordinated work.
|
|
9
|
+
- Recommend Ask, Plan, or Agent before acting.
|
|
10
|
+
- Preserve uncommitted work and use one code writer at a time.
|
|
11
|
+
- Stop for human approval between reviewable slices.
|
|
12
|
+
- Never commit, push, publish, deploy, or perform irreversible external actions without explicit approval.
|
|
13
|
+
- Use \`/tito\` when Tito coordination is wanted.
|
|
14
|
+
- Plans own technical decisions and include guidance code when useful.
|
|
15
|
+
|
|
16
|
+
Keep this bootstrap compact. Load project documentation only when the task needs it.
|
|
17
|
+
`;
|
|
18
|
+
export class PlanError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "PlanError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function configBody(profile) {
|
|
27
|
+
return `schemaVersion: 1\nprofile: ${profile}\n`;
|
|
28
|
+
}
|
|
29
|
+
function requireProfile(profile) {
|
|
30
|
+
if (profile.trim() === "") {
|
|
31
|
+
throw new PlanError("missing-profile", "Missing required profile.");
|
|
32
|
+
}
|
|
33
|
+
if (isFutureRiskProfileId(profile)) {
|
|
34
|
+
throw new PlanError("unsupported-profile", `Profile "${profile}" is recognized but not available.`);
|
|
35
|
+
}
|
|
36
|
+
if (!isActiveRiskProfileId(profile)) {
|
|
37
|
+
throw new PlanError("unknown-profile", `Unknown profile "${profile}". Active profiles: ${ACTIVE_RISK_PROFILE_IDS.join(", ")}.`);
|
|
38
|
+
}
|
|
39
|
+
return profile;
|
|
40
|
+
}
|
|
41
|
+
export function planAdoption(report, profile) {
|
|
42
|
+
const selected = requireProfile(profile);
|
|
43
|
+
const titoYaml = report.titoYaml.status === "absent"
|
|
44
|
+
? { path: "tito.yaml", action: "create", body: configBody(selected) }
|
|
45
|
+
: { path: "tito.yaml", action: "conflict" };
|
|
46
|
+
const agentsMd = report.agentsMd === "absent"
|
|
47
|
+
? { path: "AGENTS.md", action: "create", body: ADOPTION_AGENTS_MD }
|
|
48
|
+
: { path: "AGENTS.md", action: "keep" };
|
|
49
|
+
return { root: report.root, profile: selected, files: [titoYaml, agentsMd] };
|
|
50
|
+
}
|
|
51
|
+
export function formatAdoptionPlan(plan) {
|
|
52
|
+
const lines = [`root: ${plan.root}`, `profile: ${plan.profile}`];
|
|
53
|
+
for (const file of plan.files) {
|
|
54
|
+
lines.push(`${file.path}: ${file.action}`);
|
|
55
|
+
if (file.action === "create")
|
|
56
|
+
lines.push("---", file.body.trimEnd(), "---");
|
|
57
|
+
}
|
|
58
|
+
return `${lines.join("\n")}\n`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const MANDATORY_ESCALATION_TOPICS = Object.freeze([
|
|
2
|
+
"authentication-and-authorization",
|
|
3
|
+
"payments",
|
|
4
|
+
"secrets",
|
|
5
|
+
"production-data",
|
|
6
|
+
"destructive-database-changes",
|
|
7
|
+
"privacy-sensitive-information",
|
|
8
|
+
"public-infrastructure",
|
|
9
|
+
"irreversible-external-actions",
|
|
10
|
+
"accounting-and-financial-invariants",
|
|
11
|
+
]);
|
|
12
|
+
export const ACTIVE_RISK_PROFILE_IDS = [
|
|
13
|
+
"client-careful",
|
|
14
|
+
"solo-balanced",
|
|
15
|
+
"solo-fast",
|
|
16
|
+
];
|
|
17
|
+
export const FUTURE_RISK_PROFILE_IDS = ["small-team"];
|
|
18
|
+
function deepFreeze(value) {
|
|
19
|
+
if (typeof value === "object" && value !== null) {
|
|
20
|
+
for (const nested of Object.values(value))
|
|
21
|
+
deepFreeze(nested);
|
|
22
|
+
Object.freeze(value);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
export const RISK_PROFILES = deepFreeze({
|
|
27
|
+
"client-careful": {
|
|
28
|
+
id: "client-careful",
|
|
29
|
+
status: "active",
|
|
30
|
+
maxWriters: 1,
|
|
31
|
+
writerConstraint: "one-at-a-time",
|
|
32
|
+
independentReview: "required",
|
|
33
|
+
preferredAuthoredLines: { min: 100, max: 200 },
|
|
34
|
+
maxAuthoredLinesWithoutApproval: 300,
|
|
35
|
+
maxAuthoredFiles: 6,
|
|
36
|
+
stackedUnreviewedChanges: false,
|
|
37
|
+
humanApproval: "every-slice",
|
|
38
|
+
migrationsRequireRollbackPlans: true,
|
|
39
|
+
unapprovedActions: [
|
|
40
|
+
"commit",
|
|
41
|
+
"merge",
|
|
42
|
+
"push",
|
|
43
|
+
"deployment",
|
|
44
|
+
"publication",
|
|
45
|
+
"external-write",
|
|
46
|
+
],
|
|
47
|
+
safetyFloor: MANDATORY_ESCALATION_TOPICS,
|
|
48
|
+
},
|
|
49
|
+
"solo-balanced": {
|
|
50
|
+
id: "solo-balanced",
|
|
51
|
+
status: "active",
|
|
52
|
+
maxWriters: 1,
|
|
53
|
+
writerConstraint: "one-at-a-time",
|
|
54
|
+
humanReview: "feature-boundaries",
|
|
55
|
+
independentReviewFor: ["security", "data", "migrations", "architecture"],
|
|
56
|
+
diffLimits: "moderate",
|
|
57
|
+
automation: "deterministic-and-reversible",
|
|
58
|
+
safetyFloor: MANDATORY_ESCALATION_TOPICS,
|
|
59
|
+
},
|
|
60
|
+
"solo-fast": {
|
|
61
|
+
id: "solo-fast",
|
|
62
|
+
status: "active",
|
|
63
|
+
maxWriters: 1,
|
|
64
|
+
writerConstraint: "one-at-a-time",
|
|
65
|
+
agentSelfReview: "allowed",
|
|
66
|
+
humanReview: "milestones",
|
|
67
|
+
safetyFloor: MANDATORY_ESCALATION_TOPICS,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
export const FUTURE_RISK_PROFILES = deepFreeze({
|
|
71
|
+
"small-team": {
|
|
72
|
+
id: "small-team",
|
|
73
|
+
status: "unavailable",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
export function isActiveRiskProfileId(value) {
|
|
77
|
+
return ACTIVE_RISK_PROFILE_IDS.includes(value);
|
|
78
|
+
}
|
|
79
|
+
export function isFutureRiskProfileId(value) {
|
|
80
|
+
return FUTURE_RISK_PROFILE_IDS.includes(value);
|
|
81
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const CURSOR_MODES = Object.freeze(["Ask", "Plan", "Agent"]);
|
|
2
|
+
export function recommendMode(facts) {
|
|
3
|
+
const escalation = facts.escalationTopics.length > 0;
|
|
4
|
+
const criticalWork = facts.migration || facts.securitySensitive || facts.accountingInvariant;
|
|
5
|
+
const planning = facts.ambiguous ||
|
|
6
|
+
facts.multiModule ||
|
|
7
|
+
facts.architectureChange ||
|
|
8
|
+
facts.requiresSlicing;
|
|
9
|
+
const carefulHandling = escalation || criticalWork;
|
|
10
|
+
const independentReview = carefulHandling || facts.architectureChange;
|
|
11
|
+
const reasonCodes = [];
|
|
12
|
+
if (facts.approvedImplementationSlice) {
|
|
13
|
+
reasonCodes.push("approved-implementation-slice");
|
|
14
|
+
}
|
|
15
|
+
if (escalation)
|
|
16
|
+
reasonCodes.push("mandatory-escalation");
|
|
17
|
+
if (criticalWork)
|
|
18
|
+
reasonCodes.push("critical-work");
|
|
19
|
+
if (planning)
|
|
20
|
+
reasonCodes.push("planning-required");
|
|
21
|
+
if (facts.approvedImplementationSlice) {
|
|
22
|
+
return { mode: "Agent", reasonCodes, carefulHandling, independentReview };
|
|
23
|
+
}
|
|
24
|
+
if (reasonCodes.length > 0) {
|
|
25
|
+
return { mode: "Plan", reasonCodes, carefulHandling, independentReview };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
mode: "Ask",
|
|
29
|
+
reasonCodes: [
|
|
30
|
+
facts.intent === "read-only" ? "read-only-discovery" : "change-needs-approval",
|
|
31
|
+
],
|
|
32
|
+
carefulHandling: false,
|
|
33
|
+
independentReview: false,
|
|
34
|
+
};
|
|
35
|
+
}
|