@goodtek/vibeops 0.7.22 → 1.1.1
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 +51 -0
- package/README.md +51 -380
- package/dist/bootstrap/installer.js +42 -20
- package/dist/bootstrap/manifest.js +23 -3
- package/dist/cli.js +69 -154
- package/dist/commands/init.js +279 -114
- package/dist/commands/llm.js +177 -0
- package/dist/commands/status.js +181 -8
- package/dist/commands/task-add.js +89 -162
- package/dist/commands/task-done.js +112 -73
- package/dist/lib/config.js +77 -113
- package/dist/lib/env.js +32 -0
- package/dist/lib/git-config.js +21 -0
- package/dist/lib/git-host.js +12 -0
- package/dist/lib/git-policy.js +67 -0
- package/dist/lib/git-remote.js +108 -0
- package/dist/lib/git.js +44 -1
- package/dist/lib/init-clients.js +55 -0
- package/dist/lib/llm-complete.js +18 -19
- package/dist/lib/llm-connect.js +150 -0
- package/dist/lib/llm-preference.js +102 -0
- package/dist/lib/llm-status.js +19 -0
- package/dist/lib/paths.js +7 -3
- package/dist/lib/plan-llm-openai.js +0 -63
- package/dist/lib/pr-create.js +66 -0
- package/dist/lib/resolve-task.js +34 -0
- package/dist/lib/task-add-llm.js +53 -233
- package/dist/lib/task-context.js +68 -0
- package/dist/lib/task-done-llm.js +155 -0
- package/dist/lib/task-pr-llm.js +50 -0
- package/dist/lib/task-push-pr.js +131 -0
- package/dist/lib/task-scaffold.js +39 -43
- package/dist/lib/task-start.js +100 -0
- package/dist/lib/task-summary.js +12 -239
- package/dist/lib/task.js +30 -24
- package/dist/types/config.js +0 -16
- package/package.json +14 -14
- package/templates/.cursor/rules/02-git-safety.mdc +12 -0
- package/templates/AGENTS.md +27 -87
- package/templates/clients/claude/CLAUDE.md +28 -0
- package/templates/clients/cursor/.cursor/rules/01-task-source-of-truth.mdc +12 -0
- package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
- package/templates/clients/cursor/.cursor/rules/03-docs-on-done.mdc +13 -0
- package/templates/core/AGENTS.md +44 -0
- package/templates/core/docs/logs/README.md +7 -0
- package/templates/core/docs/project/03-architecture.md +15 -0
- package/templates/core/docs/project/05-current-state.md +15 -0
- package/templates/core/docs/project/06-decisions.md +9 -0
- package/templates/core/docs/tasks/TASK-000-template.md +37 -0
- package/templates/skills-shared/implement-task/SKILL.md +22 -0
- package/templates/skills-shared/plan-task/SKILL.md +19 -0
- 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-merge.js +0 -220
- 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/logs/README.md +0 -38
- 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/03-architecture.md +0 -34
- package/templates/docs/project/04-tech-stack.md +0 -29
- package/templates/docs/project/05-current-state.md +0 -35
- package/templates/docs/project/06-decisions.md +0 -20
- 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/templates/docs/tasks/TASK-000-template.md +0 -72
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { mergeRequestLabel } from "./git-host.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
async function ghPrCreate(opts) {
|
|
7
|
+
const { stdout } = await execFileAsync("gh", [
|
|
8
|
+
"pr",
|
|
9
|
+
"create",
|
|
10
|
+
"--base",
|
|
11
|
+
opts.baseBranch,
|
|
12
|
+
"--head",
|
|
13
|
+
opts.headBranch,
|
|
14
|
+
"--title",
|
|
15
|
+
opts.title,
|
|
16
|
+
"--body",
|
|
17
|
+
opts.body,
|
|
18
|
+
], { cwd: opts.cwd, maxBuffer: 4 * 1024 * 1024 });
|
|
19
|
+
const line = stdout.trim().split("\n").pop()?.trim();
|
|
20
|
+
if (line && line.startsWith("http"))
|
|
21
|
+
return line;
|
|
22
|
+
throw new Error("gh pr create did not return a URL.");
|
|
23
|
+
}
|
|
24
|
+
async function glabMrCreate(opts) {
|
|
25
|
+
const { stdout } = await execFileAsync("glab", [
|
|
26
|
+
"mr",
|
|
27
|
+
"create",
|
|
28
|
+
"--target-branch",
|
|
29
|
+
opts.baseBranch,
|
|
30
|
+
"--source-branch",
|
|
31
|
+
opts.headBranch,
|
|
32
|
+
"--title",
|
|
33
|
+
opts.title,
|
|
34
|
+
"--description",
|
|
35
|
+
opts.body,
|
|
36
|
+
], { cwd: opts.cwd, maxBuffer: 4 * 1024 * 1024 });
|
|
37
|
+
const line = stdout.trim().split("\n").pop()?.trim();
|
|
38
|
+
if (line && (line.startsWith("http") || line.includes("merge_requests")))
|
|
39
|
+
return line;
|
|
40
|
+
const match = stdout.match(/https?:\/\/[^\s]+/);
|
|
41
|
+
if (match)
|
|
42
|
+
return match[0];
|
|
43
|
+
throw new Error("glab mr create did not return a URL.");
|
|
44
|
+
}
|
|
45
|
+
export async function createMergeRequest(opts) {
|
|
46
|
+
const label = mergeRequestLabel(opts.host);
|
|
47
|
+
if (opts.dryRun) {
|
|
48
|
+
log.info(`would create ${label}: ${opts.headBranch} → ${opts.baseBranch}`);
|
|
49
|
+
return { url: `(dry-run ${label})` };
|
|
50
|
+
}
|
|
51
|
+
const url = opts.host === "gitlab" ? await glabMrCreate(opts) : await ghPrCreate(opts);
|
|
52
|
+
return { url };
|
|
53
|
+
}
|
|
54
|
+
export async function probeMergeRequestCli(host) {
|
|
55
|
+
try {
|
|
56
|
+
if (host === "gitlab") {
|
|
57
|
+
await execFileAsync("glab", ["--version"], { maxBuffer: 1024 });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
await execFileAsync("gh", ["--version"], { maxBuffer: 1024 });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { findTaskFile, loadActionableTasks, pickActiveTask, pickInProgressTask, } from "./task.js";
|
|
2
|
+
export function normalizeTaskRef(ref) {
|
|
3
|
+
const t = ref.trim();
|
|
4
|
+
if (/^task-/i.test(t) && !/^TASK-/i.test(t)) {
|
|
5
|
+
return `TASK-${t.replace(/^task-/i, "")}`;
|
|
6
|
+
}
|
|
7
|
+
return t.toUpperCase().startsWith("TASK-") ? t.toUpperCase() : `TASK-${t}`;
|
|
8
|
+
}
|
|
9
|
+
export async function resolveTask(paths, cwd, taskRef) {
|
|
10
|
+
const trimmed = taskRef?.trim();
|
|
11
|
+
if (trimmed !== undefined && trimmed.length > 0) {
|
|
12
|
+
const taskId = normalizeTaskRef(trimmed);
|
|
13
|
+
const taskFile = await findTaskFile(paths.docsTasks, taskId);
|
|
14
|
+
if (taskFile === null)
|
|
15
|
+
return null;
|
|
16
|
+
return { taskId, taskFile };
|
|
17
|
+
}
|
|
18
|
+
const tasks = await loadActionableTasks(paths.docsTasks);
|
|
19
|
+
const inProgress = pickInProgressTask(tasks);
|
|
20
|
+
if (inProgress !== null) {
|
|
21
|
+
return { taskId: inProgress.id, taskFile: inProgress.filePath };
|
|
22
|
+
}
|
|
23
|
+
const onBranch = await pickActiveTask(paths.docsTasks, cwd);
|
|
24
|
+
if (onBranch !== null && onBranch.status !== "done") {
|
|
25
|
+
return { taskId: onBranch.id, taskFile: onBranch.filePath };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function taskNotFoundMessage(taskRef) {
|
|
30
|
+
if (taskRef?.trim()) {
|
|
31
|
+
return `TASK not found: ${normalizeTaskRef(taskRef)}. Check docs/tasks/.`;
|
|
32
|
+
}
|
|
33
|
+
return "No open TASK found. Run `vibeops task add` or pass `vibeops task done TASK-NNN`.";
|
|
34
|
+
}
|
package/dist/lib/task-add-llm.js
CHANGED
|
@@ -1,59 +1,13 @@
|
|
|
1
1
|
import { join, relative } from "node:path";
|
|
2
2
|
import { readTextOrNull } from "./filesystem.js";
|
|
3
|
+
import { readConfig } from "./config.js";
|
|
3
4
|
import { llmCompleteJson } from "./llm-complete.js";
|
|
4
|
-
import {
|
|
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";
|
|
5
|
+
import { getLlmPreferenceFromConfig } from "./llm-preference.js";
|
|
8
6
|
import { projectPaths } from "./paths.js";
|
|
9
7
|
import { slugify } from "./slug.js";
|
|
10
|
-
import {
|
|
8
|
+
import { buildMinimalTaskMarkdown, ensureV3Sections, normalizeTaskHeader, titleFromIdea, } from "./task-scaffold.js";
|
|
11
9
|
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
|
-
}
|
|
10
|
+
import { extractJsonObject } from "./plan-llm-openai.js";
|
|
57
11
|
async function readSnippet(absPath, maxChars) {
|
|
58
12
|
const text = await readTextOrNull(absPath);
|
|
59
13
|
if (text === null)
|
|
@@ -61,208 +15,74 @@ async function readSnippet(absPath, maxChars) {
|
|
|
61
15
|
const t = text.trim();
|
|
62
16
|
return t.length <= maxChars ? t : `${t.slice(0, maxChars)}\n…`;
|
|
63
17
|
}
|
|
64
|
-
export async function
|
|
18
|
+
export async function loadProjectContextForLlm(cwd) {
|
|
65
19
|
const paths = projectPaths(cwd);
|
|
66
|
-
const chunks = [];
|
|
67
20
|
const candidates = [
|
|
68
21
|
join(paths.docsProject, "05-current-state.md"),
|
|
69
|
-
join(paths.docsProject, "03-current-state.md"),
|
|
70
|
-
join(paths.docsProject, "02-mvp-scope.md"),
|
|
71
22
|
join(paths.docsProject, "03-architecture.md"),
|
|
72
|
-
join(paths.docsProject, "04-tech-stack.md"),
|
|
73
23
|
join(paths.docsProject, "06-decisions.md"),
|
|
74
24
|
];
|
|
25
|
+
const chunks = [];
|
|
75
26
|
for (const p of candidates) {
|
|
76
|
-
const snip = await readSnippet(p,
|
|
77
|
-
if (snip)
|
|
27
|
+
const snip = await readSnippet(p, 3000);
|
|
28
|
+
if (snip)
|
|
78
29
|
chunks.push(`### ${relative(cwd, p)}\n\n${snip}`);
|
|
79
|
-
}
|
|
80
30
|
}
|
|
81
|
-
return chunks.length > 0 ? chunks.join("\n\n") : "(no
|
|
31
|
+
return chunks.length > 0 ? chunks.join("\n\n") : "(no project docs yet)";
|
|
82
32
|
}
|
|
83
|
-
function
|
|
84
|
-
return `You are a VibeOps TASK author.
|
|
33
|
+
function systemPrompt() {
|
|
34
|
+
return `You are a VibeOps TASK author. Reply with JSON only.
|
|
85
35
|
|
|
86
|
-
Schema:
|
|
87
36
|
{
|
|
88
|
-
"title": "short title without TASK- id
|
|
89
|
-
"slug": "kebab-case-
|
|
90
|
-
"markdown": "full TASK markdown
|
|
37
|
+
"title": "short title without TASK- id",
|
|
38
|
+
"slug": "kebab-case-max-60-chars",
|
|
39
|
+
"markdown": "full TASK markdown"
|
|
91
40
|
}
|
|
92
41
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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`;
|
|
100
|
-
}
|
|
101
|
-
function taskPlanSystemPrompt(taskId, projectName) {
|
|
102
|
-
return `You are a VibeOps TASK planner. Interview the user, then produce a complete TASK and a Cursor build prompt.
|
|
103
|
-
|
|
104
|
-
Respond with JSON only (no markdown fence).
|
|
105
|
-
|
|
106
|
-
Turn "question":
|
|
107
|
-
{
|
|
108
|
-
"turn": "question",
|
|
109
|
-
"message": "…",
|
|
110
|
-
"inputKind": "text" | "select" | "confirm",
|
|
111
|
-
"options": ["…"] // required for select; optional for confirm (Yes/No implied)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
Turn "done":
|
|
115
|
-
{
|
|
116
|
-
"turn": "done",
|
|
117
|
-
"taskMarkdown": "full TASK file for ${taskId}",
|
|
118
|
-
"buildPromptMarkdown": "markdown for Cursor — implement ${taskId} per the TASK"
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
taskMarkdown rules:
|
|
122
|
-
- # ${taskId}: … as first line
|
|
123
|
-
- All sections: ${REQUIRED_SECTIONS.join(", ")}
|
|
124
|
-
- Status: ${statusDisplay("planned")}; Result / Test Result: (not yet)
|
|
125
|
-
- Acceptance Criteria numbered and verifiable
|
|
126
|
-
- Align with docs/project; put out-of-MVP items in Out of Scope
|
|
127
|
-
|
|
128
|
-
buildPromptMarkdown rules:
|
|
129
|
-
- Standalone implementer brief for Cursor (like mvp-build.md)
|
|
130
|
-
- Reference the TASK path; list read-first docs; definition of done
|
|
131
|
-
- Project: ${projectName}
|
|
132
|
-
|
|
133
|
-
Ask enough questions to nail Scope, Acceptance Criteria, and Test Plan. Then finish with "done".`;
|
|
134
|
-
}
|
|
135
|
-
function parseQuickTaskJson(jsonText) {
|
|
136
|
-
const raw = JSON.parse(jsonText);
|
|
137
|
-
const title = typeof raw.title === "string" ? raw.title.trim() : "";
|
|
138
|
-
const slug = typeof raw.slug === "string" ? slugify(raw.slug, "task") : "";
|
|
139
|
-
const markdown = typeof raw.markdown === "string" ? raw.markdown.trim() : "";
|
|
140
|
-
if (title.length === 0)
|
|
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 };
|
|
42
|
+
markdown must include sections: Status, Goal, Scope, Out of Scope, Acceptance Criteria, Test Plan, Git Context, Result, Test Result.
|
|
43
|
+
Status body: ${statusDisplay("planned")}
|
|
44
|
+
Result and Test Result: (not yet)
|
|
45
|
+
Git Context: (populated by vibeops task add)
|
|
46
|
+
Use the exact TASK id given in the user message for the # heading.`;
|
|
145
47
|
}
|
|
146
|
-
export function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
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";
|
|
169
|
-
}
|
|
170
|
-
if (q.inputKind === "select" && q.options && q.options.length > 0) {
|
|
171
|
-
return await askSelect({
|
|
172
|
-
message: q.message,
|
|
173
|
-
nonInteractive: false,
|
|
174
|
-
choices: q.options,
|
|
175
|
-
default: q.options[0],
|
|
48
|
+
export async function llmScaffoldTask(params) {
|
|
49
|
+
try {
|
|
50
|
+
const context = await loadProjectContextForLlm(params.cwd);
|
|
51
|
+
const messages = [
|
|
52
|
+
{ role: "system", content: systemPrompt() },
|
|
53
|
+
{
|
|
54
|
+
role: "user",
|
|
55
|
+
content: [`TASK id: ${params.taskId}`, `Idea: ${params.idea}`, "", context].join("\n"),
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
const config = await readConfig(params.cwd);
|
|
59
|
+
const preference = getLlmPreferenceFromConfig(config);
|
|
60
|
+
const { text: raw, provider } = await llmCompleteJson(messages, {
|
|
61
|
+
cwd: params.cwd,
|
|
62
|
+
preference,
|
|
176
63
|
});
|
|
64
|
+
const parsed = JSON.parse(extractJsonObject(raw));
|
|
65
|
+
const title = typeof parsed.title === "string"
|
|
66
|
+
? parsed.title.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim()
|
|
67
|
+
: "";
|
|
68
|
+
const slug = typeof parsed.slug === "string" ? slugify(parsed.slug, "task") : slugify(title, "task");
|
|
69
|
+
const markdown = typeof parsed.markdown === "string" ? parsed.markdown.trim() : "";
|
|
70
|
+
if (title.length === 0 || markdown.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
const body = ensureV3Sections(normalizeTaskHeader(markdown, params.taskId, title));
|
|
73
|
+
return { title, slug, markdown: body, provider };
|
|
177
74
|
}
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
export async function llmGenerateQuickTask(params) {
|
|
181
|
-
const provider = await pickPlanLlmProvider(params.cwd, undefined, { interactiveLogin: false });
|
|
182
|
-
if (provider === null)
|
|
75
|
+
catch {
|
|
183
76
|
return null;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
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));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function fallbackTaskDraft(taskId, idea) {
|
|
80
|
+
const title = titleFromIdea(idea);
|
|
81
|
+
const draft = { id: taskId, title, idea };
|
|
82
|
+
const slug = slugify(title);
|
|
208
83
|
return {
|
|
209
84
|
title,
|
|
210
|
-
slug
|
|
211
|
-
markdown,
|
|
212
|
-
provider: used,
|
|
85
|
+
slug,
|
|
86
|
+
markdown: ensureV3Sections(normalizeTaskHeader(buildMinimalTaskMarkdown(draft), taskId, title)),
|
|
213
87
|
};
|
|
214
88
|
}
|
|
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,155 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readText, readTextOrNull, writeText } from "./filesystem.js";
|
|
3
|
+
import { readConfig } from "./config.js";
|
|
4
|
+
import { llmCompleteJson } from "./llm-complete.js";
|
|
5
|
+
import { getLlmPreferenceFromConfig } from "./llm-preference.js";
|
|
6
|
+
import { gitDiffNameOnly, readGitInfo } from "./git.js";
|
|
7
|
+
import { projectPaths, PROJECT_MEMORY_FILES } from "./paths.js";
|
|
8
|
+
import { extractJsonObject } from "./plan-llm-openai.js";
|
|
9
|
+
import { readSection, updateTaskSection } from "./task.js";
|
|
10
|
+
function localDateFilename() {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
const y = d.getFullYear();
|
|
13
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
14
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
15
|
+
return `${y}-${m}-${day}.md`;
|
|
16
|
+
}
|
|
17
|
+
async function readOptional(path) {
|
|
18
|
+
return readTextOrNull(path);
|
|
19
|
+
}
|
|
20
|
+
export async function llmCompleteTaskDone(params) {
|
|
21
|
+
const git = await readGitInfo(params.cwd);
|
|
22
|
+
let diffSummary = "";
|
|
23
|
+
if (git.isRepo) {
|
|
24
|
+
const names = await gitDiffNameOnly(params.cwd);
|
|
25
|
+
diffSummary = names.slice(0, 40).join("\n") || "(no diff vs HEAD index yet)";
|
|
26
|
+
}
|
|
27
|
+
const paths = projectPaths(params.cwd);
|
|
28
|
+
const currentState = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.currentState))) ?? "";
|
|
29
|
+
const architecture = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.architecture))) ?? "";
|
|
30
|
+
const decisions = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.decisions))) ?? "";
|
|
31
|
+
const messages = [
|
|
32
|
+
{
|
|
33
|
+
role: "system",
|
|
34
|
+
content: `You close a VibeOps TASK. Reply with JSON only:
|
|
35
|
+
{
|
|
36
|
+
"result": "markdown for TASK Result section — facts, paths, commands",
|
|
37
|
+
"testResult": "markdown for Test Result — commands and outcomes",
|
|
38
|
+
"currentStateMarkdown": "full replacement for docs/project/05-current-state.md OR null to skip",
|
|
39
|
+
"decisionsAppend": "markdown bullets to append under ## Decisions in 06-decisions.md OR null",
|
|
40
|
+
"architectureUpdate": "full replacement for 03-architecture.md OR null if structure unchanged",
|
|
41
|
+
"logLine": "one line for docs/logs/YYYY-MM-DD.md"
|
|
42
|
+
}
|
|
43
|
+
Be factual. Do not invent files not in the diff.`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
role: "user",
|
|
47
|
+
content: [
|
|
48
|
+
`TASK: ${params.taskId} — ${params.taskTitle}`,
|
|
49
|
+
`File: ${params.taskFileRel}`,
|
|
50
|
+
"",
|
|
51
|
+
"TASK body:",
|
|
52
|
+
params.taskBody.slice(0, 8000),
|
|
53
|
+
"",
|
|
54
|
+
"Changed paths (git):",
|
|
55
|
+
diffSummary,
|
|
56
|
+
"",
|
|
57
|
+
"Current 05-current-state.md:",
|
|
58
|
+
currentState.slice(0, 4000),
|
|
59
|
+
"",
|
|
60
|
+
"Current 03-architecture.md:",
|
|
61
|
+
architecture.slice(0, 4000),
|
|
62
|
+
"",
|
|
63
|
+
"Current 06-decisions.md:",
|
|
64
|
+
decisions.slice(0, 2000),
|
|
65
|
+
].join("\n"),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
try {
|
|
69
|
+
const config = await readConfig(params.cwd);
|
|
70
|
+
const preference = getLlmPreferenceFromConfig(config);
|
|
71
|
+
const { text: raw, provider } = await llmCompleteJson(messages, {
|
|
72
|
+
cwd: params.cwd,
|
|
73
|
+
preference,
|
|
74
|
+
});
|
|
75
|
+
const parsed = JSON.parse(extractJsonObject(raw));
|
|
76
|
+
const result = typeof parsed.result === "string" ? parsed.result.trim() : "";
|
|
77
|
+
const testResult = typeof parsed.testResult === "string" ? parsed.testResult.trim() : "";
|
|
78
|
+
if (result.length === 0 || testResult.length === 0)
|
|
79
|
+
return null;
|
|
80
|
+
return {
|
|
81
|
+
result,
|
|
82
|
+
testResult,
|
|
83
|
+
currentStateMarkdown: typeof parsed.currentStateMarkdown === "string" && parsed.currentStateMarkdown.trim().length > 0
|
|
84
|
+
? parsed.currentStateMarkdown.trim()
|
|
85
|
+
: null,
|
|
86
|
+
decisionsAppend: typeof parsed.decisionsAppend === "string" && parsed.decisionsAppend.trim().length > 0
|
|
87
|
+
? parsed.decisionsAppend.trim()
|
|
88
|
+
: null,
|
|
89
|
+
architectureUpdate: typeof parsed.architectureUpdate === "string" && parsed.architectureUpdate.trim().length > 0
|
|
90
|
+
? parsed.architectureUpdate.trim()
|
|
91
|
+
: null,
|
|
92
|
+
logLine: typeof parsed.logLine === "string" && parsed.logLine.trim().length > 0
|
|
93
|
+
? parsed.logLine.trim()
|
|
94
|
+
: `${params.taskId} done.`,
|
|
95
|
+
provider,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function applyTaskDoneMemory(cwd, patch) {
|
|
103
|
+
const paths = projectPaths(cwd);
|
|
104
|
+
const updated = [];
|
|
105
|
+
const skipped = [];
|
|
106
|
+
if (patch.currentStateMarkdown) {
|
|
107
|
+
const p = join(paths.root, PROJECT_MEMORY_FILES.currentState);
|
|
108
|
+
await writeText(p, `${patch.currentStateMarkdown.trimEnd()}\n`);
|
|
109
|
+
updated.push(PROJECT_MEMORY_FILES.currentState);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
skipped.push(PROJECT_MEMORY_FILES.currentState);
|
|
113
|
+
}
|
|
114
|
+
if (patch.architectureUpdate) {
|
|
115
|
+
const p = join(paths.root, PROJECT_MEMORY_FILES.architecture);
|
|
116
|
+
await writeText(p, `${patch.architectureUpdate.trimEnd()}\n`);
|
|
117
|
+
updated.push(PROJECT_MEMORY_FILES.architecture);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
skipped.push(PROJECT_MEMORY_FILES.architecture);
|
|
121
|
+
}
|
|
122
|
+
if (patch.decisionsAppend) {
|
|
123
|
+
const p = join(paths.root, PROJECT_MEMORY_FILES.decisions);
|
|
124
|
+
const existing = (await readOptional(p)) ?? "# Decisions\n\n";
|
|
125
|
+
const next = `${existing.trimEnd()}\n\n${patch.decisionsAppend.trim()}\n`;
|
|
126
|
+
await writeText(p, next);
|
|
127
|
+
updated.push(PROJECT_MEMORY_FILES.decisions);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
skipped.push(PROJECT_MEMORY_FILES.decisions);
|
|
131
|
+
}
|
|
132
|
+
const logPath = join(paths.docsLogs, localDateFilename());
|
|
133
|
+
const logExisting = (await readOptional(logPath)) ?? `# ${localDateFilename().replace(".md", "")}\n\n`;
|
|
134
|
+
await writeText(logPath, `${logExisting.trimEnd()}\n\n- ${patch.logLine}\n`);
|
|
135
|
+
updated.push(relativeLogPath(logPath, paths.root));
|
|
136
|
+
return { updated, skipped };
|
|
137
|
+
}
|
|
138
|
+
function relativeLogPath(abs, root) {
|
|
139
|
+
return abs.startsWith(root) ? abs.slice(root.length + 1) : abs;
|
|
140
|
+
}
|
|
141
|
+
export async function writeTaskResultSections(taskFile, result, testResult) {
|
|
142
|
+
await updateTaskSection(taskFile, "Result", result);
|
|
143
|
+
await updateTaskSection(taskFile, "Test Result", testResult);
|
|
144
|
+
}
|
|
145
|
+
/** Minimal fallback when LLM unavailable — git-only sections from diff. */
|
|
146
|
+
export async function fallbackResultSections(cwd, taskId, taskFile) {
|
|
147
|
+
const names = await gitDiffNameOnly(cwd);
|
|
148
|
+
const body = await readText(taskFile);
|
|
149
|
+
const goal = readSection(body, "Goal").slice(0, 200);
|
|
150
|
+
const result = [`Completed ${taskId}.`, goal.length > 0 ? `Goal: ${goal}` : "", names.length > 0 ? `Paths: ${names.slice(0, 15).join(", ")}` : ""]
|
|
151
|
+
.filter((l) => l.length > 0)
|
|
152
|
+
.join("\n");
|
|
153
|
+
const testResult = names.length > 0 ? `Changed: ${names.join(", ")}` : "Manual verification required.";
|
|
154
|
+
return { result, testResult };
|
|
155
|
+
}
|