@goodtek/vibeops 0.2.0 → 0.3.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/CHANGELOG.md +13 -0
- package/README.md +5 -4
- package/dist/cli.js +32 -5
- package/dist/commands/plan.js +242 -13
- package/dist/commands/task-start.js +16 -4
- package/dist/lib/brief.js +11 -4
- package/dist/lib/git.js +42 -0
- package/dist/lib/plan-apply-planner.js +131 -0
- package/dist/lib/plan-codex-auth.js +183 -0
- package/dist/lib/plan-codex-pkce.js +234 -0
- package/dist/lib/plan-codex-responses.js +228 -0
- package/dist/lib/plan-llm-cursor-agent.js +59 -0
- package/dist/lib/plan-llm-detect.js +74 -0
- package/dist/lib/plan-llm-models.js +237 -0
- package/dist/lib/plan-llm-normalize.js +146 -0
- package/dist/lib/plan-llm-openai.js +123 -0
- package/dist/lib/plan-llm-session.js +681 -0
- package/dist/lib/plan-llm-types.js +1 -0
- package/dist/lib/plan-post-apply-setup.js +116 -0
- package/dist/lib/prompt-builder.js +11 -1
- package/package.json +1 -1
- package/templates/.cursor/rules/03-git-safety.mdc +1 -1
- package/templates/.vibeops/workflows/task-lifecycle.md +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { AGENT_WORKFLOW_CHOICES, AUTH_REQUIREMENT_CHOICES, BACKEND_CHOICES, DATABASE_CHOICES, DB_LAYER_CHOICES, DEPLOYMENT_TARGET_CHOICES, FRONTEND_CHOICES, INTEGRATION_CHOICES, MVP_FEATURE_CHOICES, OUT_OF_SCOPE_CHOICES, PACKAGE_MANAGER_CHOICES, PROJECT_TYPE_CHOICES, RISK_AREA_CHOICES, TARGET_USER_CHOICES, } from "../types/brief.js";
|
|
2
|
+
const PLACEHOLDER_PROJECT_NAME = "Unnamed Project";
|
|
3
|
+
const PLACEHOLDER_IDEA = "(no idea provided — Planner Agent must fill this in)";
|
|
4
|
+
const PLACEHOLDER_PROBLEM = "(no core problem provided — Planner Agent must fill this in)";
|
|
5
|
+
const PLACEHOLDER_SUCCESS = "(no success criteria provided — Planner Agent must fill this in)";
|
|
6
|
+
function asStr(v, fallback) {
|
|
7
|
+
if (typeof v === "string")
|
|
8
|
+
return v.trim();
|
|
9
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
10
|
+
return String(v);
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
function asStrArray(v) {
|
|
14
|
+
if (!Array.isArray(v))
|
|
15
|
+
return [];
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const x of v) {
|
|
18
|
+
if (typeof x === "string" && x.trim().length > 0)
|
|
19
|
+
out.push(x.trim());
|
|
20
|
+
else if (typeof x === "number" || typeof x === "boolean")
|
|
21
|
+
out.push(String(x));
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function asBool(v, fallback) {
|
|
26
|
+
if (typeof v === "boolean")
|
|
27
|
+
return v;
|
|
28
|
+
if (typeof v === "string") {
|
|
29
|
+
const t = v.trim().toLowerCase();
|
|
30
|
+
if (t === "true" || t === "yes" || t === "y" || t === "1")
|
|
31
|
+
return true;
|
|
32
|
+
if (t === "false" || t === "no" || t === "n" || t === "0")
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
function pickNearestOrOriginal(value, choices) {
|
|
38
|
+
const t = value.trim();
|
|
39
|
+
if (t.length === 0)
|
|
40
|
+
return choices[0] ?? "Other";
|
|
41
|
+
const hit = choices.find((c) => c.toLowerCase() === t.toLowerCase());
|
|
42
|
+
if (hit)
|
|
43
|
+
return hit;
|
|
44
|
+
return t;
|
|
45
|
+
}
|
|
46
|
+
function normalizeToChoices(values, choices) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const v of values) {
|
|
49
|
+
out.push(pickNearestOrOriginal(v, choices));
|
|
50
|
+
}
|
|
51
|
+
return Array.from(new Set(out));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Coerces a partial LLM `projectBrief` object into a full `ProjectBrief`,
|
|
55
|
+
* snapping enum-like fields to known choice lists when possible.
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeLlmProjectBrief(raw) {
|
|
58
|
+
const assumptions = [];
|
|
59
|
+
let projectName = asStr(raw.projectName, "");
|
|
60
|
+
if (projectName.length === 0) {
|
|
61
|
+
projectName = PLACEHOLDER_PROJECT_NAME;
|
|
62
|
+
assumptions.push('projectName: missing from LLM output — defaulted to "Unnamed Project"');
|
|
63
|
+
}
|
|
64
|
+
let oneLineIdea = asStr(raw.oneLineIdea, "");
|
|
65
|
+
if (oneLineIdea.length === 0) {
|
|
66
|
+
oneLineIdea = PLACEHOLDER_IDEA;
|
|
67
|
+
assumptions.push("oneLineIdea: missing — placeholder for Planner Agent");
|
|
68
|
+
}
|
|
69
|
+
const projectType = pickNearestOrOriginal(asStr(raw.projectType, "SaaS"), PROJECT_TYPE_CHOICES);
|
|
70
|
+
let targetUsers = normalizeToChoices(asStrArray(raw.targetUsers), TARGET_USER_CHOICES);
|
|
71
|
+
if (targetUsers.length === 0) {
|
|
72
|
+
targetUsers = ["Developers"];
|
|
73
|
+
assumptions.push("targetUsers: empty — defaulted to Developers");
|
|
74
|
+
}
|
|
75
|
+
let coreProblem = asStr(raw.coreProblem, "");
|
|
76
|
+
if (coreProblem.length === 0) {
|
|
77
|
+
coreProblem = PLACEHOLDER_PROBLEM;
|
|
78
|
+
assumptions.push("coreProblem: missing — placeholder for Planner Agent");
|
|
79
|
+
}
|
|
80
|
+
let mvpFeatures = normalizeToChoices(asStrArray(raw.mvpFeatures), MVP_FEATURE_CHOICES);
|
|
81
|
+
if (mvpFeatures.length === 0) {
|
|
82
|
+
assumptions.push("mvpFeatures: empty — Planner Agent should expand from conversation");
|
|
83
|
+
}
|
|
84
|
+
let outOfScope = normalizeToChoices(asStrArray(raw.outOfScope), OUT_OF_SCOPE_CHOICES);
|
|
85
|
+
const frontend = pickNearestOrOriginal(asStr(raw.frontend, "Not sure"), FRONTEND_CHOICES);
|
|
86
|
+
const backend = pickNearestOrOriginal(asStr(raw.backend, "Not sure"), BACKEND_CHOICES);
|
|
87
|
+
const database = pickNearestOrOriginal(asStr(raw.database, "Not sure"), DATABASE_CHOICES);
|
|
88
|
+
const dbLayer = pickNearestOrOriginal(asStr(raw.dbLayer, "Not sure"), DB_LAYER_CHOICES);
|
|
89
|
+
const packageManager = pickNearestOrOriginal(asStr(raw.packageManager, "pnpm"), PACKAGE_MANAGER_CHOICES);
|
|
90
|
+
let deploymentTargets = normalizeToChoices(asStrArray(raw.deploymentTargets), DEPLOYMENT_TARGET_CHOICES);
|
|
91
|
+
if (deploymentTargets.length === 0) {
|
|
92
|
+
deploymentTargets = ["Not sure"];
|
|
93
|
+
assumptions.push("deploymentTargets: empty — defaulted to Not sure");
|
|
94
|
+
}
|
|
95
|
+
let authRequirements = normalizeToChoices(asStrArray(raw.authRequirements), AUTH_REQUIREMENT_CHOICES);
|
|
96
|
+
if (authRequirements.length === 0) {
|
|
97
|
+
authRequirements = ["Not sure"];
|
|
98
|
+
}
|
|
99
|
+
let integrations = normalizeToChoices(asStrArray(raw.integrations), INTEGRATION_CHOICES);
|
|
100
|
+
if (integrations.length === 0) {
|
|
101
|
+
integrations = ["None"];
|
|
102
|
+
}
|
|
103
|
+
const useNotion = asBool(raw.useNotion, true);
|
|
104
|
+
const useGitWorkflow = asBool(raw.useGitWorkflow, true);
|
|
105
|
+
let agentWorkflowLevel = asStr(raw.agentWorkflowLevel, "");
|
|
106
|
+
if (!AGENT_WORKFLOW_CHOICES.some((c) => c === agentWorkflowLevel)) {
|
|
107
|
+
agentWorkflowLevel = pickNearestOrOriginal(agentWorkflowLevel, AGENT_WORKFLOW_CHOICES);
|
|
108
|
+
}
|
|
109
|
+
if (!AGENT_WORKFLOW_CHOICES.some((c) => c === agentWorkflowLevel)) {
|
|
110
|
+
agentWorkflowLevel = AGENT_WORKFLOW_CHOICES[2];
|
|
111
|
+
assumptions.push("agentWorkflowLevel: LLM value unknown — defaulted to Advanced lineup");
|
|
112
|
+
}
|
|
113
|
+
let risks = normalizeToChoices(asStrArray(raw.risks), RISK_AREA_CHOICES);
|
|
114
|
+
if (risks.length === 0) {
|
|
115
|
+
risks = ["Other"];
|
|
116
|
+
assumptions.push("risks: empty — placeholder");
|
|
117
|
+
}
|
|
118
|
+
let successCriteria = asStr(raw.successCriteria, "");
|
|
119
|
+
if (successCriteria.length === 0) {
|
|
120
|
+
successCriteria = PLACEHOLDER_SUCCESS;
|
|
121
|
+
assumptions.push("successCriteria: missing — placeholder for Planner Agent");
|
|
122
|
+
}
|
|
123
|
+
const brief = {
|
|
124
|
+
projectName,
|
|
125
|
+
oneLineIdea,
|
|
126
|
+
projectType,
|
|
127
|
+
targetUsers,
|
|
128
|
+
coreProblem,
|
|
129
|
+
mvpFeatures,
|
|
130
|
+
outOfScope,
|
|
131
|
+
frontend,
|
|
132
|
+
backend,
|
|
133
|
+
database,
|
|
134
|
+
dbLayer,
|
|
135
|
+
packageManager,
|
|
136
|
+
deploymentTargets,
|
|
137
|
+
authRequirements,
|
|
138
|
+
integrations,
|
|
139
|
+
useNotion,
|
|
140
|
+
useGitWorkflow,
|
|
141
|
+
agentWorkflowLevel,
|
|
142
|
+
risks,
|
|
143
|
+
successCriteria,
|
|
144
|
+
};
|
|
145
|
+
return { brief, assumptions };
|
|
146
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
2
|
+
export async function openAiChatCompletionJson(params) {
|
|
3
|
+
const key = process.env.OPENAI_API_KEY?.trim();
|
|
4
|
+
if (!key) {
|
|
5
|
+
throw new Error("OPENAI_API_KEY is not set.");
|
|
6
|
+
}
|
|
7
|
+
const model = params.model ?? process.env.VIBEOPS_OPENAI_MODEL?.trim() ?? "gpt-4o-mini";
|
|
8
|
+
const res = await fetch(OPENAI_URL, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: `Bearer ${key}`,
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
model,
|
|
16
|
+
messages: params.messages,
|
|
17
|
+
temperature: 0.4,
|
|
18
|
+
response_format: { type: "json_object" },
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const body = await res.text().catch(() => "");
|
|
23
|
+
throw new Error(`OpenAI HTTP ${res.status}: ${body.slice(0, 500)}`);
|
|
24
|
+
}
|
|
25
|
+
const data = (await res.json());
|
|
26
|
+
const content = data.choices?.[0]?.message?.content;
|
|
27
|
+
if (typeof content !== "string" || content.trim().length === 0) {
|
|
28
|
+
throw new Error("OpenAI returned an empty message.");
|
|
29
|
+
}
|
|
30
|
+
return content.trim();
|
|
31
|
+
}
|
|
32
|
+
/** Plain assistant text (no `response_format`) — used for planner markdown apply. */
|
|
33
|
+
export async function openAiChatCompletionText(params) {
|
|
34
|
+
const key = process.env.OPENAI_API_KEY?.trim();
|
|
35
|
+
if (!key) {
|
|
36
|
+
throw new Error("OPENAI_API_KEY is not set.");
|
|
37
|
+
}
|
|
38
|
+
const model = params.model ?? process.env.VIBEOPS_OPENAI_MODEL?.trim() ?? "gpt-4o-mini";
|
|
39
|
+
const res = await fetch(OPENAI_URL, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `Bearer ${key}`,
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
model,
|
|
47
|
+
messages: params.messages,
|
|
48
|
+
temperature: 0.35,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const body = await res.text().catch(() => "");
|
|
53
|
+
throw new Error(`OpenAI HTTP ${res.status}: ${body.slice(0, 500)}`);
|
|
54
|
+
}
|
|
55
|
+
const data = (await res.json());
|
|
56
|
+
const content = data.choices?.[0]?.message?.content;
|
|
57
|
+
if (typeof content !== "string" || content.trim().length === 0) {
|
|
58
|
+
throw new Error("OpenAI returned an empty message.");
|
|
59
|
+
}
|
|
60
|
+
return content.trim();
|
|
61
|
+
}
|
|
62
|
+
export function extractJsonObject(text) {
|
|
63
|
+
const trimmed = text.trim();
|
|
64
|
+
const fence = /^```(?:json)?\s*([\s\S]*?)```$/m.exec(trimmed);
|
|
65
|
+
if (fence?.[1])
|
|
66
|
+
return fence[1].trim();
|
|
67
|
+
const start = trimmed.indexOf("{");
|
|
68
|
+
const end = trimmed.lastIndexOf("}");
|
|
69
|
+
if (start >= 0 && end > start)
|
|
70
|
+
return trimmed.slice(start, end + 1);
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
export function parsePlanLlmTurn(jsonText) {
|
|
74
|
+
const raw = JSON.parse(jsonText);
|
|
75
|
+
const turn = raw.turn;
|
|
76
|
+
if (turn === "done") {
|
|
77
|
+
const pb = raw.projectBrief;
|
|
78
|
+
if (!pb || typeof pb !== "object" || Array.isArray(pb)) {
|
|
79
|
+
throw new Error('Invalid JSON: "done" turn requires object "projectBrief".');
|
|
80
|
+
}
|
|
81
|
+
const plannerAssumptions = raw.plannerAssumptions;
|
|
82
|
+
const pa = Array.isArray(plannerAssumptions) && plannerAssumptions.every((x) => typeof x === "string")
|
|
83
|
+
? plannerAssumptions
|
|
84
|
+
: undefined;
|
|
85
|
+
return {
|
|
86
|
+
turn: "done",
|
|
87
|
+
projectBrief: pb,
|
|
88
|
+
plannerAssumptions: pa,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (turn === "confirm") {
|
|
92
|
+
const readableSummary = typeof raw.readableSummary === "string" ? raw.readableSummary.trim() : "";
|
|
93
|
+
if (readableSummary.length === 0) {
|
|
94
|
+
throw new Error('Invalid JSON: "confirm" turn needs non-empty "readableSummary" (markdown).');
|
|
95
|
+
}
|
|
96
|
+
const plannerNote = typeof raw.plannerNote === "string" ? raw.plannerNote.trim() : undefined;
|
|
97
|
+
return {
|
|
98
|
+
turn: "confirm",
|
|
99
|
+
readableSummary,
|
|
100
|
+
plannerNote: plannerNote && plannerNote.length > 0 ? plannerNote : undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (turn === "question") {
|
|
104
|
+
const message = typeof raw.message === "string" ? raw.message : "";
|
|
105
|
+
const questionType = raw.questionType;
|
|
106
|
+
if (message.length === 0) {
|
|
107
|
+
throw new Error('Invalid JSON: "question" turn needs non-empty "message".');
|
|
108
|
+
}
|
|
109
|
+
if (questionType !== "single" && questionType !== "multi" && questionType !== "text") {
|
|
110
|
+
throw new Error('Invalid JSON: questionType must be "single", "multi", or "text".');
|
|
111
|
+
}
|
|
112
|
+
const options = Array.isArray(raw.options)
|
|
113
|
+
? raw.options.filter((x) => typeof x === "string" && x.trim().length > 0)
|
|
114
|
+
: [];
|
|
115
|
+
return {
|
|
116
|
+
turn: "question",
|
|
117
|
+
message,
|
|
118
|
+
questionType,
|
|
119
|
+
options: options.length > 0 ? options : undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
throw new Error('Invalid JSON: "turn" must be "question", "confirm", or "done".');
|
|
123
|
+
}
|