@goodtek/vibeops 0.7.22 → 1.0.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 +21 -1
- package/README.md +62 -382
- package/dist/bootstrap/installer.js +4 -16
- package/dist/cli.js +26 -160
- package/dist/commands/init.js +3 -3
- package/dist/commands/status.js +145 -8
- package/dist/commands/task-add.js +64 -164
- package/dist/commands/task-done.js +117 -74
- package/dist/lib/config.js +2 -118
- package/dist/lib/git.js +1 -1
- package/dist/lib/paths.js +7 -3
- package/dist/lib/plan-llm-openai.js +0 -63
- package/dist/lib/resolve-task.js +34 -0
- package/dist/lib/task-add-llm.js +46 -233
- package/dist/lib/task-context.js +68 -0
- package/dist/lib/task-done-llm.js +148 -0
- package/dist/lib/task-merge.js +54 -159
- package/dist/lib/task-scaffold.js +39 -43
- package/dist/lib/task-start.js +72 -0
- package/dist/lib/task-summary.js +12 -239
- package/dist/lib/task.js +15 -24
- package/dist/types/config.js +0 -16
- package/package.json +3 -5
- package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
- package/templates/.cursor/rules/02-git-safety.mdc +11 -0
- package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
- package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
- package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
- package/templates/AGENTS.md +26 -87
- package/templates/docs/logs/README.md +4 -35
- package/templates/docs/project/03-architecture.md +10 -29
- package/templates/docs/project/05-current-state.md +7 -27
- package/templates/docs/project/06-decisions.md +6 -17
- package/templates/docs/tasks/TASK-000-template.md +9 -44
- package/dist/agent/loader.js +0 -71
- package/dist/agent/prompt.js +0 -66
- package/dist/commands/agent-list.js +0 -32
- package/dist/commands/agent-prompt.js +0 -59
- package/dist/commands/agent-show.js +0 -26
- package/dist/commands/done.js +0 -160
- package/dist/commands/github-init.js +0 -554
- package/dist/commands/github-status.js +0 -164
- package/dist/commands/next.js +0 -283
- package/dist/commands/notion-init.js +0 -764
- package/dist/commands/notion-sync.js +0 -405
- package/dist/commands/notion-test.js +0 -595
- package/dist/commands/plan.js +0 -146
- package/dist/commands/rollback.js +0 -126
- package/dist/commands/start.js +0 -129
- package/dist/commands/task-check.js +0 -155
- package/dist/commands/task-generate.js +0 -206
- package/dist/commands/task-pull.js +0 -277
- package/dist/commands/task-rollback.js +0 -174
- package/dist/commands/task-start.js +0 -102
- package/dist/commands/task-status.js +0 -122
- package/dist/lib/brief.js +0 -356
- package/dist/lib/cursor-task-prompt.js +0 -60
- package/dist/lib/done-follow-up.js +0 -308
- package/dist/lib/github-cli.js +0 -247
- package/dist/lib/guide-state.js +0 -69
- package/dist/lib/mvp-artifacts.js +0 -204
- package/dist/lib/mvp-constants.js +0 -15
- package/dist/lib/mvp-diff-summary.js +0 -43
- package/dist/lib/mvp-done-summary.js +0 -335
- package/dist/lib/notion-client.js +0 -459
- package/dist/lib/notion-discovery.js +0 -671
- package/dist/lib/notion-env.js +0 -140
- package/dist/lib/notion-mappers.js +0 -148
- package/dist/lib/notion-schema.js +0 -272
- package/dist/lib/notion-sync.js +0 -337
- package/dist/lib/notion-target.js +0 -247
- package/dist/lib/package-json.js +0 -133
- package/dist/lib/plan-apply-planner.js +0 -131
- package/dist/lib/plan-codex-pkce.js +0 -234
- package/dist/lib/plan-llm-models.js +0 -237
- package/dist/lib/plan-llm-normalize.js +0 -146
- package/dist/lib/plan-llm-session.js +0 -682
- package/dist/lib/plan-post-apply-setup.js +0 -116
- package/dist/lib/project-docs.js +0 -95
- package/dist/lib/prompt-builder.js +0 -135
- package/dist/lib/resolve-command-task.js +0 -40
- package/dist/lib/task-add-build-prompt.js +0 -39
- package/dist/lib/task-add-cursor.js +0 -81
- package/dist/lib/task-generator.js +0 -183
- package/dist/lib/task-prompt.js +0 -23
- package/dist/lib/task-pull.js +0 -354
- package/dist/lib/task-status-brief.js +0 -132
- package/dist/lib/workflow-guide.js +0 -485
- package/dist/status/collector.js +0 -103
- package/dist/status/format.js +0 -181
- package/dist/types/brief.js +0 -126
- package/templates/.cursor/rules/00-project-governance.mdc +0 -28
- package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
- package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
- package/templates/.cursor/rules/03-git-safety.mdc +0 -30
- package/templates/.cursor/rules/04-docs-update.mdc +0 -22
- package/templates/.vibeops/agents/architect.md +0 -47
- package/templates/.vibeops/agents/builder.md +0 -38
- package/templates/.vibeops/agents/docs.md +0 -54
- package/templates/.vibeops/agents/orchestrator.md +0 -40
- package/templates/.vibeops/agents/planner.md +0 -60
- package/templates/.vibeops/agents/recovery.md +0 -49
- package/templates/.vibeops/agents/reviewer.md +0 -47
- package/templates/.vibeops/agents/tester.md +0 -43
- package/templates/.vibeops/prompts/create-plan.md +0 -33
- package/templates/.vibeops/prompts/generate-tasks.md +0 -41
- package/templates/.vibeops/prompts/implement-task.md +0 -39
- package/templates/.vibeops/prompts/review-task.md +0 -34
- package/templates/.vibeops/prompts/rollback.md +0 -32
- package/templates/.vibeops/prompts/start-project.md +0 -39
- package/templates/.vibeops/workflows/notion-sync.md +0 -53
- package/templates/.vibeops/workflows/project-start.md +0 -73
- package/templates/.vibeops/workflows/rollback.md +0 -45
- package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
- package/templates/docs/project/00-overview.md +0 -27
- package/templates/docs/project/01-requirements.md +0 -30
- package/templates/docs/project/02-mvp-scope.md +0 -36
- package/templates/docs/project/04-tech-stack.md +0 -29
- package/templates/docs/project/07-backlog.md +0 -23
- package/templates/docs/project/08-env.md +0 -29
- package/templates/docs/project/09-deployment.md +0 -28
package/dist/lib/task-add-llm.js
CHANGED
|
@@ -1,59 +1,11 @@
|
|
|
1
1
|
import { join, relative } from "node:path";
|
|
2
2
|
import { readTextOrNull } from "./filesystem.js";
|
|
3
3
|
import { llmCompleteJson } from "./llm-complete.js";
|
|
4
|
-
import { bold, dim, log } from "./logger.js";
|
|
5
|
-
import { askInput, askSelect, yesNoSelect } from "./inquirer-helpers.js";
|
|
6
|
-
import { pickPlanLlmProvider } from "./plan-llm-session.js";
|
|
7
|
-
import { extractJsonObject } from "./plan-llm-openai.js";
|
|
8
4
|
import { projectPaths } from "./paths.js";
|
|
9
5
|
import { slugify } from "./slug.js";
|
|
10
|
-
import {
|
|
6
|
+
import { buildMinimalTaskMarkdown, ensureV3Sections, normalizeTaskHeader, titleFromIdea, } from "./task-scaffold.js";
|
|
11
7
|
import { statusDisplay } from "./task.js";
|
|
12
|
-
|
|
13
|
-
const REQUIRED_SECTIONS = [
|
|
14
|
-
"Status",
|
|
15
|
-
"MVP Phase",
|
|
16
|
-
"Goal",
|
|
17
|
-
"Background",
|
|
18
|
-
"Scope",
|
|
19
|
-
"Out of Scope",
|
|
20
|
-
"Acceptance Criteria",
|
|
21
|
-
"Files to Inspect First",
|
|
22
|
-
"Expected Files to Change",
|
|
23
|
-
"Risks",
|
|
24
|
-
"Test Plan",
|
|
25
|
-
"Rollback Plan",
|
|
26
|
-
"Implementation Plan",
|
|
27
|
-
"Result",
|
|
28
|
-
"Test Result",
|
|
29
|
-
];
|
|
30
|
-
function parseTaskPlanTurn(jsonText) {
|
|
31
|
-
const raw = JSON.parse(jsonText);
|
|
32
|
-
const turn = raw.turn;
|
|
33
|
-
if (turn === "done") {
|
|
34
|
-
const taskMarkdown = typeof raw.taskMarkdown === "string" ? raw.taskMarkdown.trim() : "";
|
|
35
|
-
const buildPromptMarkdown = typeof raw.buildPromptMarkdown === "string" ? raw.buildPromptMarkdown.trim() : "";
|
|
36
|
-
if (taskMarkdown.length === 0) {
|
|
37
|
-
throw new Error('Invalid JSON: "done" needs non-empty "taskMarkdown".');
|
|
38
|
-
}
|
|
39
|
-
if (buildPromptMarkdown.length === 0) {
|
|
40
|
-
throw new Error('Invalid JSON: "done" needs non-empty "buildPromptMarkdown".');
|
|
41
|
-
}
|
|
42
|
-
return { turn: "done", taskMarkdown, buildPromptMarkdown };
|
|
43
|
-
}
|
|
44
|
-
if (turn === "question") {
|
|
45
|
-
const message = typeof raw.message === "string" ? raw.message.trim() : "";
|
|
46
|
-
if (message.length === 0)
|
|
47
|
-
throw new Error('Invalid JSON: "question" needs "message".');
|
|
48
|
-
const kindRaw = typeof raw.inputKind === "string" ? raw.inputKind : "text";
|
|
49
|
-
const inputKind = kindRaw === "select" || kindRaw === "confirm" ? kindRaw : "text";
|
|
50
|
-
const options = Array.isArray(raw.options)
|
|
51
|
-
? raw.options.filter((x) => typeof x === "string")
|
|
52
|
-
: undefined;
|
|
53
|
-
return { turn: "question", message, inputKind, options };
|
|
54
|
-
}
|
|
55
|
-
throw new Error('LLM JSON must use turn "question" or "done".');
|
|
56
|
-
}
|
|
8
|
+
import { extractJsonObject } from "./plan-llm-openai.js";
|
|
57
9
|
async function readSnippet(absPath, maxChars) {
|
|
58
10
|
const text = await readTextOrNull(absPath);
|
|
59
11
|
if (text === null)
|
|
@@ -61,208 +13,69 @@ async function readSnippet(absPath, maxChars) {
|
|
|
61
13
|
const t = text.trim();
|
|
62
14
|
return t.length <= maxChars ? t : `${t.slice(0, maxChars)}\n…`;
|
|
63
15
|
}
|
|
64
|
-
export async function
|
|
16
|
+
export async function loadProjectContextForLlm(cwd) {
|
|
65
17
|
const paths = projectPaths(cwd);
|
|
66
|
-
const chunks = [];
|
|
67
18
|
const candidates = [
|
|
68
19
|
join(paths.docsProject, "05-current-state.md"),
|
|
69
|
-
join(paths.docsProject, "03-current-state.md"),
|
|
70
|
-
join(paths.docsProject, "02-mvp-scope.md"),
|
|
71
20
|
join(paths.docsProject, "03-architecture.md"),
|
|
72
|
-
join(paths.docsProject, "04-tech-stack.md"),
|
|
73
21
|
join(paths.docsProject, "06-decisions.md"),
|
|
74
22
|
];
|
|
23
|
+
const chunks = [];
|
|
75
24
|
for (const p of candidates) {
|
|
76
|
-
const snip = await readSnippet(p,
|
|
77
|
-
if (snip)
|
|
25
|
+
const snip = await readSnippet(p, 3000);
|
|
26
|
+
if (snip)
|
|
78
27
|
chunks.push(`### ${relative(cwd, p)}\n\n${snip}`);
|
|
79
|
-
}
|
|
80
28
|
}
|
|
81
|
-
return chunks.length > 0 ? chunks.join("\n\n") : "(no
|
|
82
|
-
}
|
|
83
|
-
function quickTaskSystemPrompt() {
|
|
84
|
-
return `You are a VibeOps TASK author. Respond with JSON only (no markdown fence).
|
|
85
|
-
|
|
86
|
-
Schema:
|
|
87
|
-
{
|
|
88
|
-
"title": "short title without TASK- id prefix",
|
|
89
|
-
"slug": "kebab-case-slug-max-60-chars-no-task-prefix",
|
|
90
|
-
"markdown": "full TASK markdown file"
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
The markdown file must:
|
|
94
|
-
- Start with # TASK-NNN: title (use the exact TASK id provided)
|
|
95
|
-
- Include every section: ${REQUIRED_SECTIONS.join(", ")}
|
|
96
|
-
- Status body: ${statusDisplay("planned")}
|
|
97
|
-
- Result and Test Result: (not yet)
|
|
98
|
-
- Keep Scope and Acceptance Criteria minimal but valid — the human will edit heavily in Cursor
|
|
99
|
-
- Do not invent TASK numbers other than the one given`;
|
|
29
|
+
return chunks.length > 0 ? chunks.join("\n\n") : "(no project docs yet)";
|
|
100
30
|
}
|
|
101
|
-
function
|
|
102
|
-
return `You are a VibeOps TASK
|
|
31
|
+
function systemPrompt() {
|
|
32
|
+
return `You are a VibeOps TASK author. Reply with JSON only.
|
|
103
33
|
|
|
104
|
-
Respond with JSON only (no markdown fence).
|
|
105
|
-
|
|
106
|
-
Turn "question":
|
|
107
34
|
{
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"options": ["…"] // required for select; optional for confirm (Yes/No implied)
|
|
35
|
+
"title": "short title without TASK- id",
|
|
36
|
+
"slug": "kebab-case-max-60-chars",
|
|
37
|
+
"markdown": "full TASK markdown"
|
|
112
38
|
}
|
|
113
39
|
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
40
|
+
markdown must include sections: Status, Goal, Scope, Out of Scope, Acceptance Criteria, Test Plan, Git Context, Result, Test Result.
|
|
41
|
+
Status body: ${statusDisplay("planned")}
|
|
42
|
+
Result and Test Result: (not yet)
|
|
43
|
+
Git Context: (populated by vibeops task add)
|
|
44
|
+
Use the exact TASK id given in the user message for the # heading.`;
|
|
119
45
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
throw new Error('LLM JSON missing "title".');
|
|
142
|
-
if (markdown.length === 0)
|
|
143
|
-
throw new Error('LLM JSON missing "markdown".');
|
|
144
|
-
return { title, slug: slug.length > 0 ? slug : slugify(title), markdown };
|
|
145
|
-
}
|
|
146
|
-
export function normalizeTaskMarkdownHeader(markdown, taskId, title) {
|
|
147
|
-
const lines = markdown.split("\n");
|
|
148
|
-
const header = `# ${taskId}: ${title}`;
|
|
149
|
-
if (lines.length > 0 && /^#\s+/.test(lines[0])) {
|
|
150
|
-
lines[0] = header;
|
|
151
|
-
return lines.join("\n");
|
|
152
|
-
}
|
|
153
|
-
return `${header}\n\n${markdown}`;
|
|
154
|
-
}
|
|
155
|
-
export function ensureTaskSections(markdown) {
|
|
156
|
-
let body = markdown;
|
|
157
|
-
for (const section of REQUIRED_SECTIONS) {
|
|
158
|
-
const re = new RegExp(`^##\\s+${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "im");
|
|
159
|
-
if (!re.test(body)) {
|
|
160
|
-
body = `${body.trimEnd()}\n\n## ${section}\n\n(not yet)\n`;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return body;
|
|
164
|
-
}
|
|
165
|
-
async function collectTaskPlanAnswer(q) {
|
|
166
|
-
if (q.inputKind === "confirm") {
|
|
167
|
-
const yes = await yesNoSelect({ message: q.message, defaultValue: true });
|
|
168
|
-
return yes ? "Yes" : "No";
|
|
46
|
+
export async function llmScaffoldTask(params) {
|
|
47
|
+
try {
|
|
48
|
+
const context = await loadProjectContextForLlm(params.cwd);
|
|
49
|
+
const messages = [
|
|
50
|
+
{ role: "system", content: systemPrompt() },
|
|
51
|
+
{
|
|
52
|
+
role: "user",
|
|
53
|
+
content: [`TASK id: ${params.taskId}`, `Idea: ${params.idea}`, "", context].join("\n"),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const { text: raw, provider } = await llmCompleteJson(messages, { cwd: params.cwd });
|
|
57
|
+
const parsed = JSON.parse(extractJsonObject(raw));
|
|
58
|
+
const title = typeof parsed.title === "string"
|
|
59
|
+
? parsed.title.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim()
|
|
60
|
+
: "";
|
|
61
|
+
const slug = typeof parsed.slug === "string" ? slugify(parsed.slug, "task") : slugify(title, "task");
|
|
62
|
+
const markdown = typeof parsed.markdown === "string" ? parsed.markdown.trim() : "";
|
|
63
|
+
if (title.length === 0 || markdown.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const body = ensureV3Sections(normalizeTaskHeader(markdown, params.taskId, title));
|
|
66
|
+
return { title, slug, markdown: body, provider };
|
|
169
67
|
}
|
|
170
|
-
|
|
171
|
-
return
|
|
172
|
-
message: q.message,
|
|
173
|
-
nonInteractive: false,
|
|
174
|
-
choices: q.options,
|
|
175
|
-
default: q.options[0],
|
|
176
|
-
});
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
177
70
|
}
|
|
178
|
-
return await askInput({ message: q.message, nonInteractive: false, required: true });
|
|
179
71
|
}
|
|
180
|
-
export
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const context = await loadTaskAddProjectContext(params.cwd);
|
|
185
|
-
const latest = params.latestDone !== undefined && params.latestDone !== null
|
|
186
|
-
? `Latest done TASK: ${params.latestDone.id} — ${params.latestDone.title}`
|
|
187
|
-
: "";
|
|
188
|
-
const messages = [
|
|
189
|
-
{ role: "system", content: quickTaskSystemPrompt() },
|
|
190
|
-
{
|
|
191
|
-
role: "user",
|
|
192
|
-
content: [
|
|
193
|
-
`TASK id: ${params.taskId}`,
|
|
194
|
-
`User intent: ${params.idea}`,
|
|
195
|
-
latest,
|
|
196
|
-
"",
|
|
197
|
-
"Project context:",
|
|
198
|
-
context,
|
|
199
|
-
]
|
|
200
|
-
.filter((l) => l.length > 0)
|
|
201
|
-
.join("\n"),
|
|
202
|
-
},
|
|
203
|
-
];
|
|
204
|
-
const { text: raw, provider: used } = await llmCompleteJson(messages, { cwd: params.cwd, provider });
|
|
205
|
-
const parsed = parseQuickTaskJson(extractJsonObject(raw));
|
|
206
|
-
const title = parsed.title.replace(/^TASK-\d+\s*[·:\-]\s*/i, "").trim() || parsed.title;
|
|
207
|
-
const markdown = ensureTaskSections(normalizeTaskMarkdownHeader(parsed.markdown, params.taskId, title));
|
|
72
|
+
export function fallbackTaskDraft(taskId, idea) {
|
|
73
|
+
const title = titleFromIdea(idea);
|
|
74
|
+
const draft = { id: taskId, title, idea };
|
|
75
|
+
const slug = slugify(title);
|
|
208
76
|
return {
|
|
209
77
|
title,
|
|
210
|
-
slug
|
|
211
|
-
markdown,
|
|
212
|
-
provider: used,
|
|
78
|
+
slug,
|
|
79
|
+
markdown: ensureV3Sections(normalizeTaskHeader(buildMinimalTaskMarkdown(draft), taskId, title)),
|
|
213
80
|
};
|
|
214
81
|
}
|
|
215
|
-
export async function gatherTaskPlanViaLlm(params) {
|
|
216
|
-
const provider = await pickPlanLlmProvider(params.cwd, undefined, { interactiveLogin: true });
|
|
217
|
-
if (provider === null)
|
|
218
|
-
return null;
|
|
219
|
-
const context = await loadTaskAddProjectContext(params.cwd);
|
|
220
|
-
const messages = [
|
|
221
|
-
{ role: "system", content: taskPlanSystemPrompt(params.taskId, params.projectName) },
|
|
222
|
-
{
|
|
223
|
-
role: "user",
|
|
224
|
-
content: [
|
|
225
|
-
`TASK id: ${params.taskId}`,
|
|
226
|
-
params.seedIdea ? `Initial idea: ${params.seedIdea}` : "Ask what the user wants to build.",
|
|
227
|
-
"",
|
|
228
|
-
"Project context:",
|
|
229
|
-
context,
|
|
230
|
-
"",
|
|
231
|
-
"Begin with your first question JSON.",
|
|
232
|
-
].join("\n"),
|
|
233
|
-
},
|
|
234
|
-
];
|
|
235
|
-
let lastQ = "";
|
|
236
|
-
for (let step = 0; step < MAX_PLAN_TURNS; step++) {
|
|
237
|
-
const { text: raw, provider: used } = await llmCompleteJson(messages, {
|
|
238
|
-
cwd: params.cwd,
|
|
239
|
-
provider,
|
|
240
|
-
});
|
|
241
|
-
const parsed = parseTaskPlanTurn(extractJsonObject(raw));
|
|
242
|
-
if (parsed.turn === "done") {
|
|
243
|
-
const titleMatch = /^#\s+TASK-\d+:\s*(.+)$/m.exec(parsed.taskMarkdown);
|
|
244
|
-
const title = titleMatch?.[1]?.trim() ?? params.taskId;
|
|
245
|
-
const markdown = ensureTaskSections(normalizeTaskMarkdownHeader(parsed.taskMarkdown, params.taskId, title));
|
|
246
|
-
return {
|
|
247
|
-
markdown,
|
|
248
|
-
buildPromptMarkdown: parsed.buildPromptMarkdown,
|
|
249
|
-
provider: used,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
log.blank();
|
|
253
|
-
log.info(dim(`Planning ${params.taskId} — question ${step + 1}`));
|
|
254
|
-
log.info(bold(parsed.message));
|
|
255
|
-
log.blank();
|
|
256
|
-
const answer = await collectTaskPlanAnswer(parsed);
|
|
257
|
-
lastQ = parsed.message;
|
|
258
|
-
messages.push({ role: "assistant", content: raw });
|
|
259
|
-
messages.push({
|
|
260
|
-
role: "user",
|
|
261
|
-
content: `Previous Q: ${lastQ}\nUser answer: ${answer}\n\nContinue with next JSON (question or done).`,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
throw new Error(`Task planning stopped after ${MAX_PLAN_TURNS} turns — try a shorter scope or run again.`);
|
|
265
|
-
}
|
|
266
|
-
export function defaultBuildPromptForTask(projectName, taskId, taskRelativePath) {
|
|
267
|
-
return buildTaskBuildPrompt({ projectName, taskId, taskRelativePath });
|
|
268
|
-
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { gitBranchExists, readGitInfo } from "./git.js";
|
|
3
|
+
import { readGitContext } from "./task.js";
|
|
4
|
+
import { isOnTaskBranch, loadActionableTasks, pickActiveTask, pickInProgressTask, } from "./task.js";
|
|
5
|
+
/** TASK that blocks `task add` — in progress or open on current task branch. */
|
|
6
|
+
export async function findBlockingTask(paths, cwd) {
|
|
7
|
+
const tasks = await loadActionableTasks(paths.docsTasks);
|
|
8
|
+
const inProgress = pickInProgressTask(tasks);
|
|
9
|
+
if (inProgress !== null)
|
|
10
|
+
return inProgress;
|
|
11
|
+
const git = await readGitInfo(cwd);
|
|
12
|
+
if (git.isRepo && typeof git.branch === "string" && git.branch.startsWith("task/")) {
|
|
13
|
+
for (const t of tasks) {
|
|
14
|
+
if (t.status === "done")
|
|
15
|
+
continue;
|
|
16
|
+
const ctx = await readGitContext(t.filePath);
|
|
17
|
+
if (isOnTaskBranch(git.branch, ctx))
|
|
18
|
+
return t;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export async function pickFocusTask(paths, cwd) {
|
|
24
|
+
return pickActiveTask(paths.docsTasks, cwd);
|
|
25
|
+
}
|
|
26
|
+
export function relPath(cwd, filePath) {
|
|
27
|
+
const r = relative(cwd, filePath).replace(/\\/g, "/");
|
|
28
|
+
return r.startsWith("..") ? filePath : r;
|
|
29
|
+
}
|
|
30
|
+
export function computeNextHint(input) {
|
|
31
|
+
if (!input.isVibeopsProject)
|
|
32
|
+
return "init";
|
|
33
|
+
if (input.focus === null)
|
|
34
|
+
return "task-add";
|
|
35
|
+
if (input.focus.status === "done")
|
|
36
|
+
return "task-add";
|
|
37
|
+
if (!input.resultFilled || !input.testFilled) {
|
|
38
|
+
return input.onTaskBranch === true ? "cursor-implement" : "cursor-plan";
|
|
39
|
+
}
|
|
40
|
+
return "task-done";
|
|
41
|
+
}
|
|
42
|
+
export function hintToText(hint, cwd, task) {
|
|
43
|
+
switch (hint) {
|
|
44
|
+
case "init":
|
|
45
|
+
return "Run `vibeops init` in this directory.";
|
|
46
|
+
case "task-add":
|
|
47
|
+
return "Run `vibeops task add` to start the next slice.";
|
|
48
|
+
case "task-done":
|
|
49
|
+
return task ? `Run \`vibeops task done ${task.id}\`.` : "Run `vibeops task done`.";
|
|
50
|
+
case "cursor-plan":
|
|
51
|
+
return task
|
|
52
|
+
? `In Cursor Ask: @${relPath(cwd, task.filePath)} — refine Scope / AC.`
|
|
53
|
+
: "Open the TASK file in Cursor Ask.";
|
|
54
|
+
case "cursor-implement":
|
|
55
|
+
return task
|
|
56
|
+
? `In Cursor Agent: @${relPath(cwd, task.filePath)} — implement, then task done.`
|
|
57
|
+
: "Implement per the TASK file in Cursor.";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function listInProgressTasks(paths) {
|
|
61
|
+
return (await loadActionableTasks(paths.docsTasks)).filter((t) => t.status === "in_progress");
|
|
62
|
+
}
|
|
63
|
+
export async function taskBranchExistsFor(cwd, task) {
|
|
64
|
+
const ctx = await readGitContext(task.filePath);
|
|
65
|
+
if (ctx === null)
|
|
66
|
+
return false;
|
|
67
|
+
return gitBranchExists(cwd, ctx.taskBranch);
|
|
68
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readText, readTextOrNull, writeText } from "./filesystem.js";
|
|
3
|
+
import { llmCompleteJson } from "./llm-complete.js";
|
|
4
|
+
import { gitDiffNameOnly, readGitInfo } from "./git.js";
|
|
5
|
+
import { projectPaths, PROJECT_MEMORY_FILES } from "./paths.js";
|
|
6
|
+
import { extractJsonObject } from "./plan-llm-openai.js";
|
|
7
|
+
import { readSection, updateTaskSection } from "./task.js";
|
|
8
|
+
function localDateFilename() {
|
|
9
|
+
const d = new Date();
|
|
10
|
+
const y = d.getFullYear();
|
|
11
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
12
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
13
|
+
return `${y}-${m}-${day}.md`;
|
|
14
|
+
}
|
|
15
|
+
async function readOptional(path) {
|
|
16
|
+
return readTextOrNull(path);
|
|
17
|
+
}
|
|
18
|
+
export async function llmCompleteTaskDone(params) {
|
|
19
|
+
const git = await readGitInfo(params.cwd);
|
|
20
|
+
let diffSummary = "";
|
|
21
|
+
if (git.isRepo) {
|
|
22
|
+
const names = await gitDiffNameOnly(params.cwd);
|
|
23
|
+
diffSummary = names.slice(0, 40).join("\n") || "(no diff vs HEAD index yet)";
|
|
24
|
+
}
|
|
25
|
+
const paths = projectPaths(params.cwd);
|
|
26
|
+
const currentState = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.currentState))) ?? "";
|
|
27
|
+
const architecture = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.architecture))) ?? "";
|
|
28
|
+
const decisions = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.decisions))) ?? "";
|
|
29
|
+
const messages = [
|
|
30
|
+
{
|
|
31
|
+
role: "system",
|
|
32
|
+
content: `You close a VibeOps TASK. Reply with JSON only:
|
|
33
|
+
{
|
|
34
|
+
"result": "markdown for TASK Result section — facts, paths, commands",
|
|
35
|
+
"testResult": "markdown for Test Result — commands and outcomes",
|
|
36
|
+
"currentStateMarkdown": "full replacement for docs/project/05-current-state.md OR null to skip",
|
|
37
|
+
"decisionsAppend": "markdown bullets to append under ## Decisions in 06-decisions.md OR null",
|
|
38
|
+
"architectureUpdate": "full replacement for 03-architecture.md OR null if structure unchanged",
|
|
39
|
+
"logLine": "one line for docs/logs/YYYY-MM-DD.md"
|
|
40
|
+
}
|
|
41
|
+
Be factual. Do not invent files not in the diff.`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
role: "user",
|
|
45
|
+
content: [
|
|
46
|
+
`TASK: ${params.taskId} — ${params.taskTitle}`,
|
|
47
|
+
`File: ${params.taskFileRel}`,
|
|
48
|
+
"",
|
|
49
|
+
"TASK body:",
|
|
50
|
+
params.taskBody.slice(0, 8000),
|
|
51
|
+
"",
|
|
52
|
+
"Changed paths (git):",
|
|
53
|
+
diffSummary,
|
|
54
|
+
"",
|
|
55
|
+
"Current 05-current-state.md:",
|
|
56
|
+
currentState.slice(0, 4000),
|
|
57
|
+
"",
|
|
58
|
+
"Current 03-architecture.md:",
|
|
59
|
+
architecture.slice(0, 4000),
|
|
60
|
+
"",
|
|
61
|
+
"Current 06-decisions.md:",
|
|
62
|
+
decisions.slice(0, 2000),
|
|
63
|
+
].join("\n"),
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
try {
|
|
67
|
+
const { text: raw, provider } = await llmCompleteJson(messages, { cwd: params.cwd });
|
|
68
|
+
const parsed = JSON.parse(extractJsonObject(raw));
|
|
69
|
+
const result = typeof parsed.result === "string" ? parsed.result.trim() : "";
|
|
70
|
+
const testResult = typeof parsed.testResult === "string" ? parsed.testResult.trim() : "";
|
|
71
|
+
if (result.length === 0 || testResult.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
return {
|
|
74
|
+
result,
|
|
75
|
+
testResult,
|
|
76
|
+
currentStateMarkdown: typeof parsed.currentStateMarkdown === "string" && parsed.currentStateMarkdown.trim().length > 0
|
|
77
|
+
? parsed.currentStateMarkdown.trim()
|
|
78
|
+
: null,
|
|
79
|
+
decisionsAppend: typeof parsed.decisionsAppend === "string" && parsed.decisionsAppend.trim().length > 0
|
|
80
|
+
? parsed.decisionsAppend.trim()
|
|
81
|
+
: null,
|
|
82
|
+
architectureUpdate: typeof parsed.architectureUpdate === "string" && parsed.architectureUpdate.trim().length > 0
|
|
83
|
+
? parsed.architectureUpdate.trim()
|
|
84
|
+
: null,
|
|
85
|
+
logLine: typeof parsed.logLine === "string" && parsed.logLine.trim().length > 0
|
|
86
|
+
? parsed.logLine.trim()
|
|
87
|
+
: `${params.taskId} done.`,
|
|
88
|
+
provider,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function applyTaskDoneMemory(cwd, patch) {
|
|
96
|
+
const paths = projectPaths(cwd);
|
|
97
|
+
const updated = [];
|
|
98
|
+
const skipped = [];
|
|
99
|
+
if (patch.currentStateMarkdown) {
|
|
100
|
+
const p = join(paths.root, PROJECT_MEMORY_FILES.currentState);
|
|
101
|
+
await writeText(p, `${patch.currentStateMarkdown.trimEnd()}\n`);
|
|
102
|
+
updated.push(PROJECT_MEMORY_FILES.currentState);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
skipped.push(PROJECT_MEMORY_FILES.currentState);
|
|
106
|
+
}
|
|
107
|
+
if (patch.architectureUpdate) {
|
|
108
|
+
const p = join(paths.root, PROJECT_MEMORY_FILES.architecture);
|
|
109
|
+
await writeText(p, `${patch.architectureUpdate.trimEnd()}\n`);
|
|
110
|
+
updated.push(PROJECT_MEMORY_FILES.architecture);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
skipped.push(PROJECT_MEMORY_FILES.architecture);
|
|
114
|
+
}
|
|
115
|
+
if (patch.decisionsAppend) {
|
|
116
|
+
const p = join(paths.root, PROJECT_MEMORY_FILES.decisions);
|
|
117
|
+
const existing = (await readOptional(p)) ?? "# Decisions\n\n";
|
|
118
|
+
const next = `${existing.trimEnd()}\n\n${patch.decisionsAppend.trim()}\n`;
|
|
119
|
+
await writeText(p, next);
|
|
120
|
+
updated.push(PROJECT_MEMORY_FILES.decisions);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
skipped.push(PROJECT_MEMORY_FILES.decisions);
|
|
124
|
+
}
|
|
125
|
+
const logPath = join(paths.docsLogs, localDateFilename());
|
|
126
|
+
const logExisting = (await readOptional(logPath)) ?? `# ${localDateFilename().replace(".md", "")}\n\n`;
|
|
127
|
+
await writeText(logPath, `${logExisting.trimEnd()}\n\n- ${patch.logLine}\n`);
|
|
128
|
+
updated.push(relativeLogPath(logPath, paths.root));
|
|
129
|
+
return { updated, skipped };
|
|
130
|
+
}
|
|
131
|
+
function relativeLogPath(abs, root) {
|
|
132
|
+
return abs.startsWith(root) ? abs.slice(root.length + 1) : abs;
|
|
133
|
+
}
|
|
134
|
+
export async function writeTaskResultSections(taskFile, result, testResult) {
|
|
135
|
+
await updateTaskSection(taskFile, "Result", result);
|
|
136
|
+
await updateTaskSection(taskFile, "Test Result", testResult);
|
|
137
|
+
}
|
|
138
|
+
/** Minimal fallback when LLM unavailable — git-only sections from diff. */
|
|
139
|
+
export async function fallbackResultSections(cwd, taskId, taskFile) {
|
|
140
|
+
const names = await gitDiffNameOnly(cwd);
|
|
141
|
+
const body = await readText(taskFile);
|
|
142
|
+
const goal = readSection(body, "Goal").slice(0, 200);
|
|
143
|
+
const result = [`Completed ${taskId}.`, goal.length > 0 ? `Goal: ${goal}` : "", names.length > 0 ? `Paths: ${names.slice(0, 15).join(", ")}` : ""]
|
|
144
|
+
.filter((l) => l.length > 0)
|
|
145
|
+
.join("\n");
|
|
146
|
+
const testResult = names.length > 0 ? `Changed: ${names.join(", ")}` : "Manual verification required.";
|
|
147
|
+
return { result, testResult };
|
|
148
|
+
}
|