@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,485 @@
1
+ import { basename, join } from "node:path";
2
+ import { pathExists } from "./filesystem.js";
3
+ import { readText } from "./filesystem.js";
4
+ import { gitBranchExists, gitGovernanceOnlyDirty, readGitInfo } from "./git.js";
5
+ import { findMvpTaskFile } from "./mvp-artifacts.js";
6
+ import { MVP_BUILD_PROMPT_REL, NEXT_TASK_SUGGESTION_REL, } from "./mvp-constants.js";
7
+ import { LAST_DONE_SUMMARY_REL } from "./mvp-done-summary.js";
8
+ import { cursorImplementPromptRel } from "./cursor-task-prompt.js";
9
+ import { branchNameForTaskFile, hasNonEmptySection, isMvpTaskId, isOnTaskBranch, loadActionableTasks, pickActiveTask, pickLatestDoneTask, readGitContext, readTaskFile, } from "./task.js";
10
+ export async function resolveMvpTask(paths) {
11
+ const file = await findMvpTaskFile(paths.docsTasks);
12
+ if (file === null)
13
+ return null;
14
+ return readTaskFile(file);
15
+ }
16
+ export async function resolveGuideTask(paths, cwd) {
17
+ const mvpFile = await findMvpTaskFile(paths.docsTasks);
18
+ if (mvpFile !== null) {
19
+ return {
20
+ mode: "mvp",
21
+ task: await readTaskFile(mvpFile),
22
+ referenceTask: null,
23
+ };
24
+ }
25
+ const actionable = await loadActionableTasks(paths.docsTasks);
26
+ const next = await pickActiveTask(paths.docsTasks, cwd);
27
+ if (next !== null) {
28
+ return { mode: "backlog", task: next, referenceTask: null };
29
+ }
30
+ const referenceTask = pickLatestDoneTask(actionable);
31
+ if (actionable.length > 0 && actionable.every((t) => t.status === "done")) {
32
+ return { mode: "post-mvp", task: null, referenceTask };
33
+ }
34
+ if (referenceTask !== null || (await hasPostMvpArtifacts(cwd))) {
35
+ return { mode: "post-mvp", task: null, referenceTask };
36
+ }
37
+ return { mode: "greenfield", task: null, referenceTask: null };
38
+ }
39
+ async function hasPostMvpArtifacts(cwd) {
40
+ return ((await pathExists(join(cwd, LAST_DONE_SUMMARY_REL))) ||
41
+ (await pathExists(join(cwd, NEXT_TASK_SUGGESTION_REL))));
42
+ }
43
+ export async function inferGuideStep(inputs) {
44
+ const { cwd, paths, config } = inputs;
45
+ const git = await readGitInfo(cwd);
46
+ const resolved = await resolveGuideTask(paths, cwd);
47
+ if (resolved.mode === "greenfield") {
48
+ return {
49
+ cwd,
50
+ paths,
51
+ config,
52
+ mode: "greenfield",
53
+ task: null,
54
+ referenceTask: null,
55
+ gitBranch: git.branch,
56
+ gitDirty: git.dirty,
57
+ step: buildStep("no-mvp", {
58
+ mode: "greenfield",
59
+ task: null,
60
+ referenceTask: null,
61
+ body: "",
62
+ gitCtx: null,
63
+ git,
64
+ hasSuggestion: false,
65
+ }),
66
+ };
67
+ }
68
+ if (resolved.mode === "post-mvp") {
69
+ const hasSuggestion = await pathExists(join(cwd, NEXT_TASK_SUGGESTION_REL));
70
+ return {
71
+ cwd,
72
+ paths,
73
+ config,
74
+ mode: "post-mvp",
75
+ task: null,
76
+ referenceTask: resolved.referenceTask,
77
+ gitBranch: git.branch,
78
+ gitDirty: git.dirty,
79
+ step: inputs.forceStepId !== undefined
80
+ ? buildStep(inputs.forceStepId, {
81
+ mode: "post-mvp",
82
+ task: null,
83
+ referenceTask: resolved.referenceTask,
84
+ body: "",
85
+ gitCtx: null,
86
+ git,
87
+ hasSuggestion,
88
+ })
89
+ : buildStep("continue-iteration", {
90
+ mode: "post-mvp",
91
+ task: null,
92
+ referenceTask: resolved.referenceTask,
93
+ body: "",
94
+ gitCtx: null,
95
+ git,
96
+ hasSuggestion,
97
+ }),
98
+ };
99
+ }
100
+ const task = resolved.task;
101
+ const body = await readText(task.filePath);
102
+ const gitCtx = await readGitContext(task.filePath);
103
+ const taskBranch = branchNameForTaskFile(task.filePath);
104
+ const branchExists = await gitBranchExists(cwd, taskBranch);
105
+ const hasSuggestion = await pathExists(join(cwd, NEXT_TASK_SUGGESTION_REL));
106
+ const step = inputs.forceStepId !== undefined
107
+ ? buildStep(inputs.forceStepId, {
108
+ mode: resolved.mode,
109
+ task,
110
+ referenceTask: null,
111
+ body,
112
+ gitCtx,
113
+ git,
114
+ hasSuggestion,
115
+ taskBranch,
116
+ branchExists,
117
+ })
118
+ : await inferStep({
119
+ cwd,
120
+ mode: resolved.mode,
121
+ task,
122
+ body,
123
+ gitCtx,
124
+ git,
125
+ hasSuggestion,
126
+ taskBranch,
127
+ branchExists,
128
+ });
129
+ return {
130
+ cwd,
131
+ paths,
132
+ config,
133
+ mode: resolved.mode,
134
+ task,
135
+ referenceTask: null,
136
+ gitBranch: git.branch,
137
+ gitDirty: git.dirty,
138
+ step,
139
+ };
140
+ }
141
+ /** Next guide step when the current one is manual (user chose Next). */
142
+ export function successorStepId(current, ctx) {
143
+ const resultOk = hasNonEmptySection(ctx.body, "Result");
144
+ const testOk = hasNonEmptySection(ctx.body, "Test Result");
145
+ switch (current) {
146
+ case "implement":
147
+ if (resultOk && testOk)
148
+ return "finish";
149
+ if (ctx.gitDirty === true)
150
+ return "commit";
151
+ return null;
152
+ case "commit":
153
+ return "finish";
154
+ default:
155
+ return null;
156
+ }
157
+ }
158
+ async function inferStep(ctx) {
159
+ const { mode, task, body, gitCtx, git, hasSuggestion, taskBranch, branchExists } = ctx;
160
+ const resultOk = hasNonEmptySection(body, "Result");
161
+ const testOk = hasNonEmptySection(body, "Test Result");
162
+ const onTaskBranch = git.branch === taskBranch;
163
+ if (branchExists && !onTaskBranch) {
164
+ return buildStep("checkout-task", {
165
+ mode,
166
+ task,
167
+ referenceTask: null,
168
+ body,
169
+ gitCtx,
170
+ git,
171
+ hasSuggestion,
172
+ taskBranch,
173
+ branchExists,
174
+ });
175
+ }
176
+ if (task.status === "done") {
177
+ if (mode === "mvp") {
178
+ return buildStep("all-done", { mode, task, referenceTask: null, body, gitCtx, git, hasSuggestion });
179
+ }
180
+ if (isOnTaskBranch(git.branch, gitCtx)) {
181
+ return buildStep("finish", { mode, task, referenceTask: null, body, gitCtx, git, hasSuggestion });
182
+ }
183
+ return buildStep("continue-iteration", {
184
+ mode: "post-mvp",
185
+ task,
186
+ referenceTask: task,
187
+ body,
188
+ gitCtx,
189
+ git,
190
+ hasSuggestion,
191
+ });
192
+ }
193
+ if ((!gitCtx || task.status === "planned") && !branchExists) {
194
+ if (git.dirty === true) {
195
+ const gov = await gitGovernanceOnlyDirty(ctx.cwd);
196
+ if (!gov.onlyGovernance) {
197
+ return buildStep("prepare-start", {
198
+ mode,
199
+ task,
200
+ referenceTask: null,
201
+ body,
202
+ gitCtx,
203
+ git,
204
+ hasSuggestion,
205
+ taskBranch,
206
+ branchExists,
207
+ });
208
+ }
209
+ }
210
+ return buildStep("start", {
211
+ mode,
212
+ task,
213
+ referenceTask: null,
214
+ body,
215
+ gitCtx,
216
+ git,
217
+ hasSuggestion,
218
+ taskBranch,
219
+ branchExists,
220
+ });
221
+ }
222
+ if (!resultOk || !testOk) {
223
+ return buildStep("implement", { mode, task, referenceTask: null, body, gitCtx, git, hasSuggestion });
224
+ }
225
+ if (git.dirty === true) {
226
+ const gov = await gitGovernanceOnlyDirty(ctx.cwd);
227
+ if (!gov.onlyGovernance) {
228
+ return buildStep("commit", {
229
+ mode,
230
+ task,
231
+ referenceTask: null,
232
+ body,
233
+ gitCtx,
234
+ git,
235
+ hasSuggestion,
236
+ taskBranch,
237
+ branchExists,
238
+ });
239
+ }
240
+ }
241
+ return buildStep("finish", { mode, task, referenceTask: null, body, gitCtx, git, hasSuggestion });
242
+ }
243
+ function buildStep(stepId, ctx) {
244
+ const { task, referenceTask, gitCtx, git, hasSuggestion } = ctx;
245
+ const id = task?.id ?? referenceTask?.id ?? null;
246
+ const isMvp = id !== null && isMvpTaskId(id);
247
+ switch (stepId) {
248
+ case "no-mvp":
249
+ return {
250
+ id: "no-mvp",
251
+ taskId: null,
252
+ title: "No TASK yet",
253
+ youDo: ["Run planning to create TASK-mvp and the build prompt."],
254
+ vibeopsCommand: "vibeops plan",
255
+ shellHints: ['vibeops plan --idea "Your product idea"'],
256
+ runnable: false,
257
+ };
258
+ case "continue-iteration": {
259
+ const ref = referenceTask ?? task;
260
+ const lines = [];
261
+ if (ref) {
262
+ lines.push(`Last finished: ${ref.id}${ref.title ? ` — ${ref.title}` : ""}.`);
263
+ }
264
+ else {
265
+ lines.push("MVP / backlog work is complete on this project.");
266
+ }
267
+ lines.push(`Read ${LAST_DONE_SUMMARY_REL} for what shipped (if present).`);
268
+ if (hasSuggestion) {
269
+ lines.push(`Review ${NEXT_TASK_SUGGESTION_REL} from the last vibeops done.`);
270
+ }
271
+ lines.push("Plan the next slice — prior summary is injected into TASK-mvp + mvp-build.md.");
272
+ const shellHints = ['vibeops plan --idea "Next feature or slice"'];
273
+ if (hasSuggestion) {
274
+ shellHints.push(`cat ${NEXT_TASK_SUGGESTION_REL}`);
275
+ }
276
+ return {
277
+ id: "continue-iteration",
278
+ taskId: ref?.id ?? null,
279
+ title: "Continue after MVP",
280
+ youDo: lines,
281
+ vibeopsCommand: "vibeops plan",
282
+ shellHints,
283
+ runnable: false,
284
+ };
285
+ }
286
+ case "prepare-start":
287
+ return {
288
+ id: "prepare-start",
289
+ taskId: id,
290
+ title: `Prepare git before ${id}`,
291
+ youDo: [
292
+ git.branch
293
+ ? `On ${git.branch} with uncommitted project files (not just .vibeops/).`
294
+ : "Uncommitted project files on the working tree.",
295
+ "Commit or stash them before `vibeops start` creates a task branch.",
296
+ "If this is TASK-009 (Next.js scaffold), finish with `vibeops done TASK-009` or commit that work first.",
297
+ `Optional: vibeops start ${id} --allow-dirty (carries current changes into the new task branch).`,
298
+ ].filter((l) => l.length > 0),
299
+ vibeopsCommand: null,
300
+ shellHints: [
301
+ "git status",
302
+ 'git stash push -u -m "wip before task branch"',
303
+ `vibeops start ${id} --allow-dirty`,
304
+ ],
305
+ runnable: false,
306
+ };
307
+ case "start":
308
+ return {
309
+ id: "start",
310
+ taskId: id,
311
+ title: isMvp ? "Start MVP branch" : `Start ${id}`,
312
+ youDo: isMvp
313
+ ? ["Creates task/mvp-* branch and Git Context."]
314
+ : [`Creates task branch and Git Context for ${id}.`],
315
+ vibeopsCommand: isMvp ? "vibeops start" : `vibeops start ${id}`,
316
+ shellHints: [],
317
+ runnable: true,
318
+ };
319
+ case "checkout-task": {
320
+ const branch = ctx.taskBranch ?? gitCtx?.taskBranch ?? "task/…";
321
+ return {
322
+ id: "checkout-task",
323
+ taskId: id,
324
+ title: `Resume ${id} branch`,
325
+ youDo: [
326
+ git.branch ? `Current branch: ${git.branch}` : "",
327
+ `Switch: git switch ${branch} (or: vibeops start ${id})`,
328
+ "Does not recreate the branch if it already exists.",
329
+ ].filter((l) => l.length > 0),
330
+ vibeopsCommand: isMvp ? "vibeops start" : `vibeops start ${id}`,
331
+ shellHints: [`git switch ${branch}`],
332
+ runnable: true,
333
+ };
334
+ }
335
+ case "implement": {
336
+ const branchNote = gitCtx && git.branch && git.branch !== gitCtx.taskBranch
337
+ ? `On ${git.branch} — switch: git switch ${gitCtx.taskBranch}`
338
+ : gitCtx
339
+ ? `Branch: ${gitCtx.taskBranch}`
340
+ : "";
341
+ const taskFile = task ? basename(task.filePath) : "TASK file";
342
+ const fillResult = !hasNonEmptySection(ctx.body, "Result");
343
+ const fillTestResult = !hasNonEmptySection(ctx.body, "Test Result");
344
+ const needsCursorPrompt = fillResult || fillTestResult;
345
+ const promptRel = task && needsCursorPrompt ? cursorImplementPromptRel(task.id) : null;
346
+ const implementLines = isMvp
347
+ ? [
348
+ branchNote,
349
+ `Drag ${MVP_BUILD_PROMPT_REL} into a new chat.`,
350
+ ...(needsCursorPrompt && promptRel
351
+ ? [
352
+ `Result / Test Result empty — in Cursor: @${promptRel} (see ${promptRel}).`,
353
+ ]
354
+ : ["Meet Acceptance Criteria; fill Result and Test Result."]),
355
+ ]
356
+ : [
357
+ branchNote,
358
+ `Open docs/tasks/${taskFile} in Cursor.`,
359
+ ...(needsCursorPrompt && promptRel
360
+ ? [
361
+ `Result / Test Result empty — in Cursor: @${promptRel} or paste the prompt from that file.`,
362
+ ]
363
+ : ["Meet Acceptance Criteria; fill Result and Test Result."]),
364
+ ];
365
+ return {
366
+ id: "implement",
367
+ taskId: id,
368
+ title: isMvp ? "Build MVP in Cursor" : `Implement ${id}`,
369
+ youDo: implementLines.filter((l) => l.length > 0),
370
+ vibeopsCommand: null,
371
+ shellHints: promptRel ? [`cat ${promptRel}`] : [],
372
+ runnable: false,
373
+ };
374
+ }
375
+ case "commit": {
376
+ const doneCmd = isMvp ? "vibeops done" : `vibeops done ${id}`;
377
+ return {
378
+ id: "commit",
379
+ taskId: id,
380
+ title: "Commit work on task branch",
381
+ youDo: [
382
+ "Commit task files (docs/, src/) — not node_modules or .next.",
383
+ `Or run ${doneCmd} to commit safe paths and merge.`,
384
+ ],
385
+ vibeopsCommand: null,
386
+ shellHints: [
387
+ "git status",
388
+ "git add docs/ src/ …",
389
+ `git commit -m "feat(${id?.toLowerCase() ?? "task"}): <summary>"`,
390
+ doneCmd,
391
+ ],
392
+ runnable: false,
393
+ };
394
+ }
395
+ case "finish": {
396
+ const markedDone = task?.status === "done";
397
+ return {
398
+ id: "finish",
399
+ taskId: id,
400
+ title: isMvp
401
+ ? "Finish MVP (merge)"
402
+ : markedDone
403
+ ? `Close out ${id} (merge & sync)`
404
+ : `Finish ${id}`,
405
+ youDo: markedDone
406
+ ? [
407
+ "TASK file already says Done — still on the task branch.",
408
+ "Run done to commit if needed, merge to base branch, delete task branch.",
409
+ "Runs Notion sync when enabled.",
410
+ ]
411
+ : [
412
+ "Requires Result and Test Result.",
413
+ isMvp
414
+ ? "Marks Done, commits if needed, merges to main, deletes task branch."
415
+ : "Marks Done, commits if needed, merges to main when on the task branch.",
416
+ "Runs Notion sync when enabled; offers post-MVP cleanup if the tree is still dirty.",
417
+ ],
418
+ vibeopsCommand: isMvp ? "vibeops done" : `vibeops done ${id}`,
419
+ shellHints: [],
420
+ runnable: true,
421
+ };
422
+ }
423
+ case "notion-sync":
424
+ return {
425
+ id: "notion-sync",
426
+ taskId: id,
427
+ title: "Sync Notion metadata",
428
+ youDo: ["Optional — pushes project + TASK metadata to Notion."],
429
+ vibeopsCommand: "vibeops notion sync",
430
+ shellHints: ["vibeops notion sync --dry-run"],
431
+ runnable: true,
432
+ };
433
+ case "all-done":
434
+ return {
435
+ id: "all-done",
436
+ taskId: id,
437
+ title: "MVP complete",
438
+ youDo: [
439
+ `Read ${LAST_DONE_SUMMARY_REL} for what shipped.`,
440
+ "Update docs/project/05-current-state.md if the project stage changed.",
441
+ "Next iteration: vibeops plan (prior summary is injected into TASK + mvp-build.md).",
442
+ ],
443
+ vibeopsCommand: "vibeops plan",
444
+ shellHints: hasSuggestion ? [`cat ${NEXT_TASK_SUGGESTION_REL}`] : [],
445
+ runnable: false,
446
+ };
447
+ }
448
+ }
449
+ /** Higher = later in the TASK workflow (used to keep manual Next advances). */
450
+ export function guideStepRank(stepId) {
451
+ switch (stepId) {
452
+ case "no-mvp":
453
+ case "continue-iteration":
454
+ return 0;
455
+ case "prepare-start":
456
+ return 5;
457
+ case "start":
458
+ case "checkout-task":
459
+ return 10;
460
+ case "implement":
461
+ return 20;
462
+ case "commit":
463
+ return 30;
464
+ case "finish":
465
+ case "notion-sync":
466
+ return 40;
467
+ case "all-done":
468
+ return 50;
469
+ default:
470
+ return 0;
471
+ }
472
+ }
473
+ export function followUpStepId(completed) {
474
+ switch (completed) {
475
+ case "start":
476
+ case "checkout-task":
477
+ return "implement";
478
+ case "commit":
479
+ return "implement";
480
+ case "finish":
481
+ return "all-done";
482
+ default:
483
+ return null;
484
+ }
485
+ }
@@ -4,7 +4,7 @@ import { readConfig } from "../lib/config.js";
4
4
  import { readGitInfo } from "../lib/git.js";
5
5
  import { getNotionTokenSource } from "../lib/notion-env.js";
6
6
  import { readPackageJson, } from "../lib/package-json.js";
7
- import { countTasks, pickNextTask, scanTasks } from "../lib/task.js";
7
+ import { countTasks, pickActiveTask, scanTasks } from "../lib/task.js";
8
8
  import { projectPaths } from "../lib/paths.js";
9
9
  import { DEFAULT_GITHUB_CONFIG, } from "../types/config.js";
10
10
  async function presenceCheck(label, path, required, asDir) {
@@ -78,7 +78,7 @@ export async function collectStatus(cwd) {
78
78
  ? await scanTasks(paths.docsTasks)
79
79
  : [];
80
80
  const taskCounts = countTasks(tasks);
81
- const nextTask = pickNextTask(tasks);
81
+ const nextTask = await pickActiveTask(paths.docsTasks, paths.root);
82
82
  const git = await readGitInfo(paths.root);
83
83
  const tokenProbe = await getNotionTokenSource(paths.root);
84
84
  const notion = snapshotNotion(config?.notion, tokenProbe);
@@ -70,10 +70,14 @@ export function printHuman(report) {
70
70
  log.info(` next ${report.nextTask.id} — ${report.nextTask.title || dim("(no title)")} ${dim(`[${report.nextTask.status}]`)}`);
71
71
  }
72
72
  else if (total === 0) {
73
- log.info(` ${dim("no TASK files yet — run `vibeops task generate`")}`);
73
+ log.info(` ${dim("no TASK files — run `vibeops plan`")}`);
74
74
  }
75
75
  else {
76
- log.info(` ${dim("all tasks done")}`);
76
+ log.info(` ${dim("all tracked tasks done")}`);
77
+ }
78
+ const mvp = report.tasks.find((t) => t.id.toUpperCase() === "TASK-MVP");
79
+ if (mvp && mvp.status !== "done") {
80
+ log.info(` workflow ${cyan("vibeops start")} → ${cyan(".vibeops/generated/mvp-build.md")} in Cursor → ${cyan("vibeops done")}`);
77
81
  }
78
82
  log.blank();
79
83
  log.info(bold("Git"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodtek/vibeops",
3
- "version": "0.3.0",
3
+ "version": "0.7.22",
4
4
  "description": "Workflow rails for Cursor-based vibe coding projects.",
5
5
  "type": "module",
6
6
  "author": "VibeOps contributors",
@@ -40,6 +40,7 @@
40
40
  "workflow"
41
41
  ],
42
42
  "dependencies": {
43
+ "@goodtek/vibeops": "^0.3.0",
43
44
  "@inquirer/prompts": "^8.4.3",
44
45
  "@notionhq/client": "^5.20.0",
45
46
  "commander": "^12.1.0",
@@ -1,7 +1,7 @@
1
1
  # TASK-NNN · <one-line title>
2
2
 
3
3
  > This file is an example produced from VibeOps' `TASK-000-template.md`.
4
- > Real TASKs are generated by `vibeops task generate`. Filenames follow the `TASK-NNN-<slug>.md` form.
4
+ > Real TASKs come from `vibeops plan` (TASK-mvp), `vibeops task add` (work-now slice), or your backlog process. Filenames follow `TASK-NNN-<slug>.md`.
5
5
 
6
6
  ## Status
7
7