@goodtek/vibeops 0.3.0 → 0.7.22

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.
@@ -0,0 +1,126 @@
1
+ import { relative, resolve } from "node:path";
2
+ import { gitBranchExists, gitCheckout, gitDeleteBranch, gitResetHard, readGitInfo, } from "../lib/git.js";
3
+ import { commandTaskNotFoundMessage, resolveCommandTask, } from "../lib/resolve-command-task.js";
4
+ import { bold, cyan, dim, log, red, yellow } from "../lib/logger.js";
5
+ import { projectPaths } from "../lib/paths.js";
6
+ import { readGitContext, readTaskFile, statusDisplay } from "../lib/task.js";
7
+ function relOrAbs(root, p) {
8
+ const r = relative(root, p);
9
+ return r === "" ? "." : r.startsWith("..") ? p : r;
10
+ }
11
+ function shellQuote(s) {
12
+ return /^[A-Za-z0-9._/\-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
13
+ }
14
+ function strategyCommands(strategy, ctx) {
15
+ switch (strategy) {
16
+ case "branch-delete":
17
+ return [
18
+ `git switch ${shellQuote(ctx.baseBranch)}`,
19
+ `git branch -D ${shellQuote(ctx.taskBranch)}`,
20
+ ];
21
+ case "reset-base":
22
+ return [
23
+ `git switch ${shellQuote(ctx.taskBranch)}`,
24
+ `git reset --hard ${shellQuote(ctx.baseCommit)}`,
25
+ ];
26
+ case "revert-merge":
27
+ return [
28
+ `git switch ${shellQuote(ctx.baseBranch)}`,
29
+ `git log --oneline --merges ${shellQuote(ctx.baseBranch)} | head -5`,
30
+ `git revert -m 1 <merge-sha>`,
31
+ ];
32
+ }
33
+ }
34
+ function strategyRisk(strategy) {
35
+ switch (strategy) {
36
+ case "branch-delete":
37
+ return "Deletes the task branch and any unmerged work on it.";
38
+ case "reset-base":
39
+ return "Hard-resets the task branch to the base commit.";
40
+ case "revert-merge":
41
+ return "Manual revert on main after a merge was pushed.";
42
+ }
43
+ }
44
+ const STRATEGIES = ["branch-delete", "reset-base", "revert-merge"];
45
+ export async function rollbackCommand(taskRef, options = {}) {
46
+ const cwd = resolve(options.cwd ?? process.cwd());
47
+ const paths = projectPaths(cwd);
48
+ const resolved = await resolveCommandTask(paths, cwd, taskRef);
49
+ if (resolved === null) {
50
+ log.error(commandTaskNotFoundMessage(taskRef));
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+ const { taskFile } = resolved;
55
+ const meta = await readTaskFile(taskFile);
56
+ const ctx = await readGitContext(taskFile);
57
+ const git = await readGitInfo(cwd);
58
+ log.info(bold(`vibeops rollback ${meta.id}`));
59
+ log.info(` ${dim("file")} ${relOrAbs(cwd, taskFile)}`);
60
+ log.info(` ${dim("status")} ${statusDisplay(meta.status)}`);
61
+ log.blank();
62
+ if (ctx === null) {
63
+ log.warn(`No Git Context in TASK file. Run ${cyan("vibeops start")} first.`);
64
+ if (options.dryRun !== true) {
65
+ process.exitCode = 1;
66
+ }
67
+ return;
68
+ }
69
+ log.info(bold("Git Context"));
70
+ log.info(` base ${ctx.baseBranch} @ ${ctx.baseCommit}`);
71
+ log.info(` task ${cyan(ctx.taskBranch)}`);
72
+ log.blank();
73
+ for (const s of STRATEGIES) {
74
+ const destructive = s === "reset-base";
75
+ log.info(` ${bold(s)} ${destructive ? red("destructive") : yellow("safe")}`);
76
+ log.info(` ${dim(strategyRisk(s))}`);
77
+ for (const cmd of strategyCommands(s, ctx)) {
78
+ log.info(` ${dim("$")} ${cmd}`);
79
+ }
80
+ log.blank();
81
+ }
82
+ if (options.confirm !== true && options.confirmDestructive !== true) {
83
+ log.info(`Guidance only. Use ${cyan("--confirm")} or ${cyan("--confirm-destructive")} to run.`);
84
+ return;
85
+ }
86
+ const strategy = options.strategy ?? "branch-delete";
87
+ if (!STRATEGIES.includes(strategy)) {
88
+ log.error(`Unknown strategy: ${strategy}`);
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ if (strategy === "revert-merge") {
93
+ log.info(`${yellow("!")} revert-merge is manual only — run the commands above.`);
94
+ return;
95
+ }
96
+ if (strategy === "reset-base" && options.confirmDestructive !== true) {
97
+ log.error("reset-base requires --confirm-destructive.");
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ if (!git.isRepo) {
102
+ log.error("Not a git repository.");
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ if (options.dryRun === true) {
107
+ log.info(bold(`dry-run (${strategy}):`));
108
+ for (const cmd of strategyCommands(strategy, ctx)) {
109
+ log.info(` ${dim("$")} ${cmd}`);
110
+ }
111
+ return;
112
+ }
113
+ if (strategy === "branch-delete") {
114
+ if (git.branch === ctx.taskBranch)
115
+ await gitCheckout(cwd, ctx.baseBranch);
116
+ if (!options.keepBranch && (await gitBranchExists(cwd, ctx.taskBranch))) {
117
+ await gitDeleteBranch(cwd, ctx.taskBranch, { force: true });
118
+ log.ok(`Deleted ${ctx.taskBranch}`);
119
+ }
120
+ return;
121
+ }
122
+ if (git.branch !== ctx.taskBranch)
123
+ await gitCheckout(cwd, ctx.taskBranch);
124
+ await gitResetHard(cwd, ctx.baseCommit);
125
+ log.ok(`Reset ${ctx.taskBranch} → ${ctx.baseCommit}`);
126
+ }
@@ -0,0 +1,129 @@
1
+ import { join, relative, resolve } from "node:path";
2
+ import { branchNameForTaskFile, isMvpTaskId, readGitContext, readTaskFile, upsertGitContext, updateInlineStatus, } from "../lib/task.js";
3
+ import { detectDefaultBranch, gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitGovernanceOnlyDirty, gitHeadCommit, readGitInfo, } from "../lib/git.js";
4
+ import { MVP_BUILD_PROMPT_REL } from "../lib/mvp-constants.js";
5
+ import { taskBuildPromptRel } from "../lib/task-add-build-prompt.js";
6
+ import { pathExists } from "../lib/filesystem.js";
7
+ import { bold, cyan, dim, log } from "../lib/logger.js";
8
+ import { projectPaths } from "../lib/paths.js";
9
+ import { commandTaskNotFoundMessage, resolveCommandTask, } from "../lib/resolve-command-task.js";
10
+ function relOrAbs(root, p) {
11
+ const r = relative(root, p);
12
+ return r === "" ? "." : r.startsWith("..") ? p : r;
13
+ }
14
+ export async function startCommand(taskRef, options = {}) {
15
+ const cwd = resolve(options.cwd ?? process.cwd());
16
+ const paths = projectPaths(cwd);
17
+ const resolved = await resolveCommandTask(paths, cwd, taskRef);
18
+ if (resolved === null) {
19
+ log.error(commandTaskNotFoundMessage(taskRef));
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ const { taskId, taskFile, source } = resolved;
24
+ if (source === "backlog-active") {
25
+ log.info(dim(`No TASK-mvp — continuing backlog with ${cyan(taskId)}.`));
26
+ }
27
+ const git = await readGitInfo(cwd);
28
+ if (!git.isRepo) {
29
+ if (options.dryRun === true) {
30
+ log.info(bold("dry-run — would:"));
31
+ log.info(` · require a git repo under ${cwd}`);
32
+ log.info(` · git checkout -b task/mvp-*`);
33
+ log.info(` · Status → In Progress + Git Context`);
34
+ return;
35
+ }
36
+ log.error(`Not a git repository: ${cwd}`);
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ if (git.dirty === true && options.allowDirty !== true) {
41
+ const gov = await gitGovernanceOnlyDirty(cwd);
42
+ if (!gov.onlyGovernance) {
43
+ log.error("Git working tree is dirty. Commit or stash, or use --allow-dirty.");
44
+ if (gov.nonGovernancePaths.length > 0) {
45
+ for (const p of gov.nonGovernancePaths.slice(0, 16)) {
46
+ log.info(` ${dim("·")} ${p}`);
47
+ }
48
+ if (gov.nonGovernancePaths.length > 16) {
49
+ log.info(` ${dim("·")} … and ${gov.nonGovernancePaths.length - 16} more`);
50
+ }
51
+ }
52
+ log.blank();
53
+ log.info(dim("Options:"));
54
+ log.info(` ${dim("1.")} Commit on ${git.branch ?? "HEAD"}: git add … && git commit`);
55
+ log.info(` ${dim("2.")} Stash: git stash push -u -m "wip"`);
56
+ log.info(` ${dim("3.")} Carry WIP into task branch: ${cyan(`vibeops start ${taskId} --allow-dirty`)}`);
57
+ log.info(dim(" If this is TASK-009 scaffold on main, run vibeops done TASK-009 or commit it before TASK-010."));
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+ log.warn("Only governance / VibeOps paths are dirty — proceeding.");
62
+ }
63
+ const baseBranch = git.branch ?? (await detectDefaultBranch(cwd)) ?? "main";
64
+ const baseCommit = (await gitHeadCommit(cwd)) ?? "";
65
+ if (baseCommit.length === 0) {
66
+ log.error("No commits on HEAD yet.");
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+ const taskBranch = branchNameForTaskFile(taskFile);
71
+ const startedAt = new Date().toISOString();
72
+ const ctx = { baseBranch, baseCommit, taskBranch, startedAt };
73
+ const meta = await readTaskFile(taskFile);
74
+ const existingCtx = await readGitContext(taskFile);
75
+ const branchExists = await gitBranchExists(cwd, taskBranch);
76
+ log.info(bold(`vibeops start`));
77
+ log.info(` ${dim("task")} ${taskId}`);
78
+ log.info(` ${dim("file")} ${relOrAbs(cwd, taskFile)}`);
79
+ if (branchExists && existingCtx) {
80
+ log.info(` ${dim("base branch")} ${existingCtx.baseBranch} @ ${existingCtx.baseCommit.slice(0, 7)}`);
81
+ }
82
+ else {
83
+ log.info(` ${dim("base branch")} ${baseBranch} @ ${baseCommit.slice(0, 7)}`);
84
+ }
85
+ log.info(` ${dim("task branch")} ${cyan(taskBranch)}`);
86
+ log.blank();
87
+ if (options.dryRun === true) {
88
+ log.info(bold("dry-run — would:"));
89
+ log.info(branchExists
90
+ ? ` · git switch ${taskBranch} (branch exists)`
91
+ : ` · git checkout -b ${taskBranch}`);
92
+ log.info(` · Status → In Progress + Git Context in TASK file`);
93
+ return;
94
+ }
95
+ if (branchExists) {
96
+ await gitCheckout(cwd, taskBranch);
97
+ if (meta.status === "planned") {
98
+ await updateInlineStatus(taskFile, "in_progress");
99
+ }
100
+ if (existingCtx === null) {
101
+ await upsertGitContext(taskFile, ctx);
102
+ }
103
+ if (existingCtx?.baseBranch.startsWith("chore/vibeops-post-")) {
104
+ log.warn(`Git Context base is ${existingCtx.baseBranch} — merge or delete that chore branch, then fix base in the TASK file if needed.`);
105
+ }
106
+ log.ok(`Resumed existing branch ${taskBranch}.`);
107
+ }
108
+ else {
109
+ await gitCheckoutNewBranch(cwd, taskBranch, baseBranch);
110
+ await updateInlineStatus(taskFile, "in_progress");
111
+ await upsertGitContext(taskFile, ctx);
112
+ log.ok(`Branch ${taskBranch} ready.`);
113
+ }
114
+ log.blank();
115
+ log.info(bold("Build in Cursor"));
116
+ if (isMvpTaskId(taskId)) {
117
+ log.info(` Drag ${cyan(relOrAbs(cwd, MVP_BUILD_PROMPT_REL))} into a new chat.`);
118
+ }
119
+ else {
120
+ const taskBuildRel = taskBuildPromptRel(taskId);
121
+ if (await pathExists(join(cwd, taskBuildRel))) {
122
+ log.info(` Drag ${cyan(relOrAbs(cwd, taskBuildRel))} into a new chat.`);
123
+ }
124
+ else {
125
+ log.info(` Open ${cyan(relOrAbs(cwd, taskFile))} and implement per Acceptance Criteria.`);
126
+ }
127
+ }
128
+ log.info(` When done: fill Result / Test Result, then ${cyan(`vibeops done ${taskId}`)}.`);
129
+ }
@@ -0,0 +1,200 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { doneCommand } from "./done.js";
4
+ import { startCommand } from "./start.js";
5
+ import { pathExists, writeText } from "../lib/filesystem.js";
6
+ import { bold, cyan, dim, log } from "../lib/logger.js";
7
+ import { askInput, askSelect, yesNoSelect } from "../lib/inquirer-helpers.js";
8
+ import { projectPaths } from "../lib/paths.js";
9
+ import { readConfig } from "../lib/config.js";
10
+ import { slugify } from "../lib/slug.js";
11
+ import { defaultBuildPromptForTask, gatherTaskPlanViaLlm, llmGenerateQuickTask, } from "../lib/task-add-llm.js";
12
+ import { taskBuildPromptAbs, taskBuildPromptRel, } from "../lib/task-add-build-prompt.js";
13
+ import { allocateNextTaskNumber, buildWorkNowTaskMarkdown, formatTaskId, titleFromIdea, uniqueTaskPath, } from "../lib/task-scaffold.js";
14
+ import { readGitInfo } from "../lib/git.js";
15
+ import { loadActionableTasks, pickInProgressTask, pickLatestDoneTask, } from "../lib/task.js";
16
+ function relOrAbs(root, p) {
17
+ const r = relative(root, p);
18
+ return r === "" ? "." : r.startsWith("..") ? p : r;
19
+ }
20
+ async function maybeCloseInProgressTask(root, inProgress, nonInteractive) {
21
+ if (nonInteractive)
22
+ return true;
23
+ log.info(`Current TASK ${bold(inProgress.id)} is ${cyan("In Progress")}: ${dim(inProgress.title)}`);
24
+ log.blank();
25
+ const closeFirst = await yesNoSelect({
26
+ message: "Close this TASK first (commit, merge, clean tree) before starting a new one?",
27
+ defaultValue: true,
28
+ });
29
+ if (!closeFirst) {
30
+ log.warn(`Keeping ${inProgress.id} open — finish it or run ${cyan("vibeops done")} before switching branches.`);
31
+ return true;
32
+ }
33
+ log.blank();
34
+ log.step(`Closing ${inProgress.id} via vibeops done…`);
35
+ await doneCommand(inProgress.id, { cwd: root, allowDirty: true });
36
+ if (process.exitCode !== undefined && process.exitCode !== 0) {
37
+ log.error("Could not close the current TASK — fix blockers above, then run task add again.");
38
+ return false;
39
+ }
40
+ log.blank();
41
+ return true;
42
+ }
43
+ async function pickAddMode(nonInteractive) {
44
+ if (nonInteractive)
45
+ return "just";
46
+ const choice = await askSelect({
47
+ message: "How do you want to create the new TASK?",
48
+ nonInteractive: false,
49
+ choices: ["Just create task", "Create plan using LLM"],
50
+ default: "Just create task",
51
+ });
52
+ return choice.startsWith("Create plan") ? "plan" : "just";
53
+ }
54
+ export async function taskAddCommand(opts = {}) {
55
+ const root = resolve(opts.cwd ?? process.cwd());
56
+ const dryRun = opts.dryRun === true;
57
+ const nonInteractive = opts.nonInteractive === true || dryRun || process.stdin.isTTY !== true;
58
+ log.info(bold("vibeops task add"));
59
+ log.blank();
60
+ let tasks = await loadActionableTasks(projectPaths(root).docsTasks);
61
+ const inProgress = pickInProgressTask(tasks);
62
+ if (inProgress !== null) {
63
+ const ok = await maybeCloseInProgressTask(root, inProgress, nonInteractive);
64
+ if (!ok)
65
+ return;
66
+ tasks = await loadActionableTasks(projectPaths(root).docsTasks);
67
+ }
68
+ const mode = await pickAddMode(nonInteractive);
69
+ const latestDone = pickLatestDoneTask(tasks);
70
+ const nextNum = allocateNextTaskNumber(tasks);
71
+ const taskId = formatTaskId(nextNum);
72
+ const ideaDefault = opts.idea?.trim() ||
73
+ (latestDone ? `Follow-up after ${latestDone.id}` : "New work slice");
74
+ const idea = await askInput({
75
+ message: mode === "plan"
76
+ ? "What do you want to build? (starting point for planning)"
77
+ : "What are you doing now? (short)",
78
+ nonInteractive,
79
+ default: ideaDefault,
80
+ required: true,
81
+ });
82
+ const paths = projectPaths(root);
83
+ const config = await readConfig(root);
84
+ const projectName = config?.name ?? "project";
85
+ let title = titleFromIdea(idea);
86
+ let slug = slugify(title);
87
+ let markdown = "";
88
+ let buildPromptMarkdown = null;
89
+ let llmNote = null;
90
+ if (mode === "plan") {
91
+ if (dryRun) {
92
+ log.info(`[dry-run] Would run LLM planning for ${bold(taskId)}`);
93
+ log.info(dim(` idea: ${idea}`));
94
+ return;
95
+ }
96
+ if (nonInteractive) {
97
+ log.error("Create plan using LLM requires an interactive terminal (omit --non-interactive).");
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ log.step("LLM task planning (interactive questions)…");
102
+ const planned = await gatherTaskPlanViaLlm({
103
+ cwd: root,
104
+ taskId,
105
+ projectName,
106
+ seedIdea: idea,
107
+ });
108
+ if (planned === null) {
109
+ log.error("No LLM provider available for planning.");
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+ markdown = planned.markdown;
114
+ buildPromptMarkdown = planned.buildPromptMarkdown;
115
+ llmNote = `planned via ${planned.provider}`;
116
+ const header = /^#\s+TASK-\d+:\s*(.+)$/m.exec(markdown);
117
+ if (header?.[1])
118
+ title = header[1].trim();
119
+ slug = slugify(title);
120
+ }
121
+ else {
122
+ let quick = null;
123
+ if (!nonInteractive || opts.idea) {
124
+ quick = await llmGenerateQuickTask({
125
+ cwd: root,
126
+ taskId,
127
+ idea,
128
+ latestDone,
129
+ });
130
+ }
131
+ if (quick !== null) {
132
+ title = quick.title;
133
+ slug = quick.slug;
134
+ markdown = quick.markdown;
135
+ llmNote = `quick scaffold via ${quick.provider}`;
136
+ }
137
+ else {
138
+ const parent = inProgress ?? latestDone;
139
+ markdown = buildWorkNowTaskMarkdown({
140
+ id: taskId,
141
+ title,
142
+ idea,
143
+ mvpPhase: parent?.mvpPhase?.trim() || "Work-now slice",
144
+ spawnedFrom: parent?.id,
145
+ });
146
+ if (!nonInteractive) {
147
+ log.warn("LLM unavailable — using minimal TASK template.");
148
+ }
149
+ }
150
+ }
151
+ const { filePath } = uniqueTaskPath(paths.docsTasks, taskId, slug, tasks.map((t) => t.filePath));
152
+ const relFile = relOrAbs(root, filePath);
153
+ const taskRelative = relFile.replace(/\\/g, "/");
154
+ const buildRel = taskBuildPromptRel(taskId);
155
+ const buildAbs = taskBuildPromptAbs(root, taskId);
156
+ if (buildPromptMarkdown === null) {
157
+ buildPromptMarkdown = defaultBuildPromptForTask(projectName, taskId, taskRelative);
158
+ }
159
+ if (dryRun) {
160
+ log.info(`[dry-run] Would create ${bold(taskId)} → ${cyan(relFile)}`);
161
+ if (mode === "plan") {
162
+ log.info(`[dry-run] Would write ${cyan(buildRel)}`);
163
+ }
164
+ log.blank();
165
+ return;
166
+ }
167
+ if (await pathExists(filePath)) {
168
+ throw new Error(`TASK file already exists: ${relFile}`);
169
+ }
170
+ await writeText(filePath, markdown);
171
+ await mkdir(join(root, ".vibeops", "generated"), { recursive: true });
172
+ await writeText(buildAbs, buildPromptMarkdown);
173
+ log.ok(`Created ${bold(taskId)} → ${cyan(relFile)}`);
174
+ if (mode === "plan") {
175
+ log.ok(`Cursor build doc → ${cyan(buildRel)}`);
176
+ }
177
+ if (llmNote)
178
+ log.skip(llmNote);
179
+ const git = await readGitInfo(root);
180
+ if (!git.isRepo) {
181
+ log.warn("Not a git repository — TASK file saved. Run vibeops init --git, then start.");
182
+ return;
183
+ }
184
+ log.blank();
185
+ log.step(`Starting ${taskId} (branch + In Progress)…`);
186
+ await startCommand(taskId, { cwd: root, allowDirty: true });
187
+ if (process.exitCode !== undefined && process.exitCode !== 0) {
188
+ log.warn(`TASK file exists but start failed — run ${cyan(`vibeops start ${taskId}`)} when Git is ready.`);
189
+ return;
190
+ }
191
+ log.blank();
192
+ if (mode === "plan") {
193
+ log.info(bold("Build in Cursor"));
194
+ log.info(` Drag ${cyan(buildRel)} into a new chat and implement.`);
195
+ }
196
+ else {
197
+ log.info(bold("Work in Cursor"));
198
+ log.info(` Edit ${cyan(relFile)} freely, then ${cyan(`vibeops done ${taskId}`)} when finished.`);
199
+ }
200
+ }
@@ -0,0 +1,122 @@
1
+ import { resolve } from "node:path";
2
+ import { collectTaskStatusBrief, taskStatusBriefToJson, } from "../lib/task-status-brief.js";
3
+ import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
4
+ function sectionFilled(ok) {
5
+ return ok ? green("filled") : yellow("empty");
6
+ }
7
+ function printHuman(brief) {
8
+ log.blank();
9
+ log.info(bold("─".repeat(56)));
10
+ log.info(bold(" VibeOps · TASK briefing"));
11
+ log.info(bold("─".repeat(56)));
12
+ if (!brief.isVibeopsProject) {
13
+ log.info(` ${dim("Project")} ${yellow("not a VibeOps project")} (.vibeops.json missing)`);
14
+ if (brief.git.isRepo && brief.git.branch) {
15
+ log.info(` ${dim("Git")} ${brief.git.branch}`);
16
+ }
17
+ log.blank();
18
+ log.info(` ${dim("Next")} ${cyan("vibeops init")}`);
19
+ log.info(bold("─".repeat(56)));
20
+ log.blank();
21
+ return;
22
+ }
23
+ if (brief.projectName) {
24
+ log.info(` ${dim("Project")} ${brief.projectName}`);
25
+ }
26
+ if (brief.counts) {
27
+ const c = brief.counts;
28
+ log.info(` ${dim("Backlog")} ${c.total} tasks · ${cyan(String(c.in_progress))} in progress · ${c.planned} planned · ${c.done} done`);
29
+ }
30
+ if (brief.inProgressTasks.length > 1) {
31
+ log.blank();
32
+ log.info(bold(" In progress (multiple)"));
33
+ for (const t of brief.inProgressTasks) {
34
+ log.info(` ${cyan(t.id)} — ${t.title || dim("(no title)")}`);
35
+ }
36
+ }
37
+ log.blank();
38
+ if (brief.activeTask) {
39
+ const t = brief.activeTask;
40
+ log.info(` ${dim("Focus")} ${cyan(t.id)} — ${t.title || dim("(no title)")}`);
41
+ log.info(` ${dim("Status")} ${t.status}`);
42
+ if (t.mvpPhase)
43
+ log.info(` ${dim("Phase")} ${t.mvpPhase}`);
44
+ log.info(` ${dim("File")} ${t.file}`);
45
+ }
46
+ else if (brief.mode === "post-mvp" && brief.referenceTask) {
47
+ log.info(` ${dim("Focus")} ${dim("MVP/backlog complete — last:")} ${cyan(brief.referenceTask.id)}`);
48
+ }
49
+ else {
50
+ log.info(` ${dim("Focus")} ${dim("(none — run vibeops plan or task add)")}`);
51
+ }
52
+ if (brief.sections) {
53
+ log.blank();
54
+ log.info(bold(" Progress"));
55
+ if (brief.sections.goalExcerpt.length > 0) {
56
+ log.info(` ${dim("Goal")} ${brief.sections.goalExcerpt}`);
57
+ }
58
+ log.info(` ${dim("Result")} ${sectionFilled(brief.sections.resultFilled)} · ${dim("Test Result")} ${sectionFilled(brief.sections.testResultFilled)}`);
59
+ if (brief.sections.acceptanceCriteriaCount > 0) {
60
+ log.info(` ${dim("AC")} ${brief.sections.acceptanceCriteriaCount} acceptance criteria`);
61
+ }
62
+ }
63
+ log.blank();
64
+ log.info(bold(" Git"));
65
+ if (!brief.git.isRepo) {
66
+ log.info(` ${yellow("not a git repository")}`);
67
+ }
68
+ else {
69
+ const branch = brief.git.branch ?? "(detached)";
70
+ log.info(` ${dim("HEAD")} ${branch}`);
71
+ if (brief.git.dirty === true)
72
+ log.info(` ${dim("Tree")} ${yellow("dirty")}`);
73
+ else if (brief.git.dirty === false)
74
+ log.info(` ${dim("Tree")} ${green("clean")}`);
75
+ if (brief.git.expectedTaskBranch) {
76
+ const on = brief.git.onTaskBranch === true
77
+ ? green("yes")
78
+ : brief.git.onTaskBranch === false
79
+ ? yellow("no")
80
+ : dim("—");
81
+ const exists = brief.git.taskBranchExists === true
82
+ ? dim("exists")
83
+ : brief.git.taskBranchExists === false
84
+ ? dim("not created")
85
+ : "";
86
+ log.info(` ${dim("Task br.")} ${brief.git.expectedTaskBranch} · on branch: ${on}${exists ? ` · ${exists}` : ""}`);
87
+ }
88
+ if (brief.git.baseBranch) {
89
+ log.info(` ${dim("Base")} ${brief.git.baseBranch}`);
90
+ }
91
+ }
92
+ if (brief.artifacts.length > 0) {
93
+ log.blank();
94
+ log.info(bold(" Cursor artifacts"));
95
+ for (const a of brief.artifacts)
96
+ log.info(` ${cyan(a)}`);
97
+ }
98
+ log.blank();
99
+ log.info(bold(` Step: ${brief.workflow.stepTitle}`));
100
+ for (const line of brief.workflow.youDo) {
101
+ log.info(` ${dim("·")} ${line}`);
102
+ }
103
+ if (brief.workflow.suggestedCommand) {
104
+ log.blank();
105
+ log.info(` ${dim("Suggested")} ${cyan(brief.workflow.suggestedCommand)}`);
106
+ }
107
+ log.info(bold("─".repeat(56)));
108
+ log.blank();
109
+ }
110
+ export async function taskStatusCommand(opts = {}) {
111
+ const cwd = resolve(opts.cwd ?? process.cwd());
112
+ const brief = await collectTaskStatusBrief(cwd);
113
+ if (opts.json === true) {
114
+ log.raw(taskStatusBriefToJson(brief));
115
+ }
116
+ else {
117
+ printHuman(brief);
118
+ }
119
+ if (!brief.isVibeopsProject) {
120
+ process.exitCode = 1;
121
+ }
122
+ }
package/dist/lib/brief.js CHANGED
@@ -195,7 +195,7 @@ export async function gatherBrief(inputs) {
195
195
  const meta = {
196
196
  vibeopsVersion: VERSION,
197
197
  generatedAt: new Date().toISOString(),
198
- source: inputs.legacyWizard === true ? "legacy-wizard" : inputs.nonInteractive ? "non-interactive" : "interactive",
198
+ source: inputs.nonInteractive ? "non-interactive" : "interactive",
199
199
  schemaVersion: PROJECT_BRIEF_SCHEMA_VERSION,
200
200
  assumptions,
201
201
  };
@@ -0,0 +1,60 @@
1
+ import { join, relative } from "node:path";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { pathExists, writeText } from "./filesystem.js";
4
+ import { hasNonEmptySection } from "./task.js";
5
+ import { VERSION } from "../version.js";
6
+ export function cursorImplementPromptRel(taskId) {
7
+ const slug = taskId.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
8
+ return `.vibeops/generated/cursor-implement-${slug}.md`;
9
+ }
10
+ export function buildCursorImplementPrompt(opts) {
11
+ const fill = [];
12
+ if (opts.fillResult)
13
+ fill.push("**Result**");
14
+ if (opts.fillTestResult)
15
+ fill.push("**Test Result**");
16
+ const fillLine = fill.length > 0
17
+ ? `4. Update ${fill.join(" and ")} in \`${opts.taskPath}\` (replace Pending / (not yet) placeholders).`
18
+ : "";
19
+ return `# Implement ${opts.taskId}
20
+
21
+ Project: \`${opts.projectName}\` (VibeOps ${opts.vibeopsVersion})
22
+
23
+ Read and follow the **entire** TASK file:
24
+
25
+ \`${opts.taskPath}\`
26
+
27
+ Procedure:
28
+
29
+ 1. Work only inside **Scope** and **Acceptance Criteria** in that file.
30
+ 2. **Search** the repo for existing patterns before adding code or docs.
31
+ 3. Do the work required by this TASK (implementation, spec, or docs — per Scope / Out of Scope).
32
+ ${fillLine ? `${fillLine}\n` : ""}5. Do not touch other TASKs. No git push or merge.
33
+
34
+ When finished, run \`vibeops next\` and choose Refresh, then \`vibeops done ${opts.taskId}\`.
35
+ `;
36
+ }
37
+ /** Write \`.vibeops/generated/cursor-implement-*.md\` when Result/Test need filling. */
38
+ export async function ensureCursorImplementPrompt(cwd, config, task, body) {
39
+ const fillResult = !hasNonEmptySection(body, "Result");
40
+ const fillTestResult = !hasNonEmptySection(body, "Test Result");
41
+ if (!fillResult && !fillTestResult)
42
+ return null;
43
+ const rel = cursorImplementPromptRel(task.id);
44
+ const taskPath = relative(cwd, task.filePath).replace(/\\/g, "/");
45
+ const projectName = config?.name ?? "project";
46
+ const text = buildCursorImplementPrompt({
47
+ projectName,
48
+ taskId: task.id,
49
+ taskPath,
50
+ vibeopsVersion: VERSION,
51
+ fillResult,
52
+ fillTestResult,
53
+ });
54
+ const outDir = join(cwd, ".vibeops", "generated");
55
+ if (!(await pathExists(outDir))) {
56
+ await mkdir(outDir, { recursive: true });
57
+ }
58
+ await writeText(join(cwd, rel), text);
59
+ return { rel, fillResult, fillTestResult };
60
+ }