@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.
Files changed (121) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +62 -382
  3. package/dist/bootstrap/installer.js +4 -16
  4. package/dist/cli.js +26 -160
  5. package/dist/commands/init.js +3 -3
  6. package/dist/commands/status.js +145 -8
  7. package/dist/commands/task-add.js +64 -164
  8. package/dist/commands/task-done.js +117 -74
  9. package/dist/lib/config.js +2 -118
  10. package/dist/lib/git.js +1 -1
  11. package/dist/lib/paths.js +7 -3
  12. package/dist/lib/plan-llm-openai.js +0 -63
  13. package/dist/lib/resolve-task.js +34 -0
  14. package/dist/lib/task-add-llm.js +46 -233
  15. package/dist/lib/task-context.js +68 -0
  16. package/dist/lib/task-done-llm.js +148 -0
  17. package/dist/lib/task-merge.js +54 -159
  18. package/dist/lib/task-scaffold.js +39 -43
  19. package/dist/lib/task-start.js +72 -0
  20. package/dist/lib/task-summary.js +12 -239
  21. package/dist/lib/task.js +15 -24
  22. package/dist/types/config.js +0 -16
  23. package/package.json +3 -5
  24. package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  25. package/templates/.cursor/rules/02-git-safety.mdc +11 -0
  26. package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
  27. package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
  28. package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
  29. package/templates/AGENTS.md +26 -87
  30. package/templates/docs/logs/README.md +4 -35
  31. package/templates/docs/project/03-architecture.md +10 -29
  32. package/templates/docs/project/05-current-state.md +7 -27
  33. package/templates/docs/project/06-decisions.md +6 -17
  34. package/templates/docs/tasks/TASK-000-template.md +9 -44
  35. package/dist/agent/loader.js +0 -71
  36. package/dist/agent/prompt.js +0 -66
  37. package/dist/commands/agent-list.js +0 -32
  38. package/dist/commands/agent-prompt.js +0 -59
  39. package/dist/commands/agent-show.js +0 -26
  40. package/dist/commands/done.js +0 -160
  41. package/dist/commands/github-init.js +0 -554
  42. package/dist/commands/github-status.js +0 -164
  43. package/dist/commands/next.js +0 -283
  44. package/dist/commands/notion-init.js +0 -764
  45. package/dist/commands/notion-sync.js +0 -405
  46. package/dist/commands/notion-test.js +0 -595
  47. package/dist/commands/plan.js +0 -146
  48. package/dist/commands/rollback.js +0 -126
  49. package/dist/commands/start.js +0 -129
  50. package/dist/commands/task-check.js +0 -155
  51. package/dist/commands/task-generate.js +0 -206
  52. package/dist/commands/task-pull.js +0 -277
  53. package/dist/commands/task-rollback.js +0 -174
  54. package/dist/commands/task-start.js +0 -102
  55. package/dist/commands/task-status.js +0 -122
  56. package/dist/lib/brief.js +0 -356
  57. package/dist/lib/cursor-task-prompt.js +0 -60
  58. package/dist/lib/done-follow-up.js +0 -308
  59. package/dist/lib/github-cli.js +0 -247
  60. package/dist/lib/guide-state.js +0 -69
  61. package/dist/lib/mvp-artifacts.js +0 -204
  62. package/dist/lib/mvp-constants.js +0 -15
  63. package/dist/lib/mvp-diff-summary.js +0 -43
  64. package/dist/lib/mvp-done-summary.js +0 -335
  65. package/dist/lib/notion-client.js +0 -459
  66. package/dist/lib/notion-discovery.js +0 -671
  67. package/dist/lib/notion-env.js +0 -140
  68. package/dist/lib/notion-mappers.js +0 -148
  69. package/dist/lib/notion-schema.js +0 -272
  70. package/dist/lib/notion-sync.js +0 -337
  71. package/dist/lib/notion-target.js +0 -247
  72. package/dist/lib/package-json.js +0 -133
  73. package/dist/lib/plan-apply-planner.js +0 -131
  74. package/dist/lib/plan-codex-pkce.js +0 -234
  75. package/dist/lib/plan-llm-models.js +0 -237
  76. package/dist/lib/plan-llm-normalize.js +0 -146
  77. package/dist/lib/plan-llm-session.js +0 -682
  78. package/dist/lib/plan-post-apply-setup.js +0 -116
  79. package/dist/lib/project-docs.js +0 -95
  80. package/dist/lib/prompt-builder.js +0 -135
  81. package/dist/lib/resolve-command-task.js +0 -40
  82. package/dist/lib/task-add-build-prompt.js +0 -39
  83. package/dist/lib/task-add-cursor.js +0 -81
  84. package/dist/lib/task-generator.js +0 -183
  85. package/dist/lib/task-prompt.js +0 -23
  86. package/dist/lib/task-pull.js +0 -354
  87. package/dist/lib/task-status-brief.js +0 -132
  88. package/dist/lib/workflow-guide.js +0 -485
  89. package/dist/status/collector.js +0 -103
  90. package/dist/status/format.js +0 -181
  91. package/dist/types/brief.js +0 -126
  92. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  93. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  94. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  95. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  96. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  97. package/templates/.vibeops/agents/architect.md +0 -47
  98. package/templates/.vibeops/agents/builder.md +0 -38
  99. package/templates/.vibeops/agents/docs.md +0 -54
  100. package/templates/.vibeops/agents/orchestrator.md +0 -40
  101. package/templates/.vibeops/agents/planner.md +0 -60
  102. package/templates/.vibeops/agents/recovery.md +0 -49
  103. package/templates/.vibeops/agents/reviewer.md +0 -47
  104. package/templates/.vibeops/agents/tester.md +0 -43
  105. package/templates/.vibeops/prompts/create-plan.md +0 -33
  106. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  107. package/templates/.vibeops/prompts/implement-task.md +0 -39
  108. package/templates/.vibeops/prompts/review-task.md +0 -34
  109. package/templates/.vibeops/prompts/rollback.md +0 -32
  110. package/templates/.vibeops/prompts/start-project.md +0 -39
  111. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  112. package/templates/.vibeops/workflows/project-start.md +0 -73
  113. package/templates/.vibeops/workflows/rollback.md +0 -45
  114. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  115. package/templates/docs/project/00-overview.md +0 -27
  116. package/templates/docs/project/01-requirements.md +0 -30
  117. package/templates/docs/project/02-mvp-scope.md +0 -36
  118. package/templates/docs/project/04-tech-stack.md +0 -29
  119. package/templates/docs/project/07-backlog.md +0 -23
  120. package/templates/docs/project/08-env.md +0 -29
  121. package/templates/docs/project/09-deployment.md +0 -28
package/dist/cli.js CHANGED
@@ -1,34 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { doneCommand } from "./commands/done.js";
4
- import { githubInitCommand } from "./commands/github-init.js";
5
- import { githubStatusCommand } from "./commands/github-status.js";
6
3
  import { initCommand } from "./commands/init.js";
7
- import { nextCommand } from "./commands/next.js";
8
- import { notionInitCommand } from "./commands/notion-init.js";
9
- import { notionSyncCommand } from "./commands/notion-sync.js";
10
- import { notionTestCommand } from "./commands/notion-test.js";
11
- import { planCommand } from "./commands/plan.js";
12
- import { rollbackCommand } from "./commands/rollback.js";
13
- import { startCommand } from "./commands/start.js";
14
4
  import { statusCommand } from "./commands/status.js";
15
5
  import { taskAddCommand } from "./commands/task-add.js";
16
- import { taskStatusCommand } from "./commands/task-status.js";
6
+ import { taskDoneCommand } from "./commands/task-done.js";
17
7
  import { VERSION } from "./version.js";
18
8
  const program = new Command();
19
9
  program
20
10
  .name("vibeops")
21
- .description("VibeOps — plan an MVP, build in Cursor (drag mvp-build.md), finish with Git rails.\n" +
22
- " Commands: init · plan · start · done · next · status · task add · task status (+ optional notion · github · rollback)")
11
+ .description("VibeOps v3 — Cursor-friendly TASK workflow (init · task add · task done · status)")
23
12
  .version(VERSION, "-v, --version", "Print the VibeOps version");
24
13
  program
25
14
  .command("init")
26
- .description("Install the VibeOps workflow into the current directory")
27
- .option("--dry-run", "Show what would be created without writing files")
15
+ .description("Install the VibeOps v3 project tree")
16
+ .option("--dry-run", "Show what would be created")
28
17
  .option("--force", "Overwrite existing files")
29
18
  .option("--cwd <path>", "Target directory")
30
19
  .option("--name <projectName>", "Project name in .vibeops.json")
31
- .option("--git", "Initialize Git without prompting")
20
+ .option("--git", "Initialize Git")
32
21
  .option("--no-git", "Skip Git")
33
22
  .option("--initial-commit", "Create initial commit")
34
23
  .option("--no-initial-commit", "Skip initial commit")
@@ -37,80 +26,13 @@ program
37
26
  .action(async (opts) => {
38
27
  await initCommand(opts);
39
28
  });
40
- program
41
- .command("plan")
42
- .description("LLM planning → brief + TASK-mvp + .vibeops/generated/mvp-build.md")
43
- .option("--idea <text>", "One-line idea (optional Name: idea prefix)")
44
- .option("--from <path>", "Regenerate MVP artifacts from an existing brief")
45
- .option("--non-interactive", "Placeholders only (CI); no LLM")
46
- .option("--provider <id>", "openai | codex-oauth | cursor-agent")
47
- .option("--model <id>", "LLM model id")
48
- .option("--cwd <path>", "Target directory")
49
- .action(async (opts) => {
50
- const provider = opts.provider === "openai" ||
51
- opts.provider === "codex-oauth" ||
52
- opts.provider === "cursor-agent"
53
- ? opts.provider
54
- : undefined;
55
- await planCommand({
56
- idea: opts.idea,
57
- from: opts.from,
58
- nonInteractive: opts.nonInteractive,
59
- provider,
60
- llmModel: opts.model,
61
- cwd: opts.cwd,
62
- });
63
- });
64
- program
65
- .command("start [taskRef]")
66
- .description("Create task branch (default: TASK-mvp, or active backlog TASK if no MVP file)")
67
- .option("--dry-run", "Show plan only")
68
- .option("--allow-dirty", "Allow dirty working tree")
69
- .option("--cwd <path>", "Target directory")
70
- .action(async (taskRef, opts) => {
71
- await startCommand(taskRef, opts);
72
- });
73
- program
74
- .command("done [taskRef]")
75
- .description("Finish TASK: auto-fill Result/Test Result + summary md, merge to main, Notion sync (default: TASK-mvp or active backlog)")
76
- .option("--dry-run", "Show plan only")
77
- .option("--no-merge", "Skip push / merge / branch delete")
78
- .option("--skip-summary", "Skip last-done-summary.md and TASK auto-fill")
79
- .option("--refresh-task-sections", "Overwrite existing Result / Test Result")
80
- .option("--merge-via-pr", "Merge via gh PR")
81
- .option("--allow-dirty", "Allow dirty tree during merge")
82
- .option("--no-notion-sync", "Skip automatic Notion sync after done")
83
- .option("--cwd <path>", "Target directory")
84
- .action(async (taskRef, opts) => {
85
- await doneCommand(taskRef, opts);
86
- });
87
- program
88
- .command("next")
89
- .description("Interactive guide for the MVP workflow (↑/↓ · Yes/No)")
90
- .option("--dry-run", "On runnable steps, print only")
91
- .option("--non-interactive", "Print panel once")
92
- .option("--execute", "With --non-interactive, run the runnable step once")
93
- .option("--merge-via-pr", "On finish: merge via GitHub PR")
94
- .option("--allow-dirty", "Allow dirty tree on merge")
95
- .option("--cwd <path>", "Target directory")
96
- .action(async (opts) => {
97
- await nextCommand(opts);
98
- });
99
- program
100
- .command("status")
101
- .description("Installation, TASK-mvp state, Git, Notion, GitHub")
102
- .option("--json", "JSON output")
103
- .option("--cwd <path>", "Target directory")
104
- .action(async (opts) => {
105
- await statusCommand(opts);
106
- });
107
- const task = program.command("task").description("Backlog TASK files");
29
+ const task = program.command("task").description("TASK lifecycle");
108
30
  task
109
31
  .command("add")
110
- .description("Interactive: close open TASK (optional), create next TASK-NNN, branch checkout, In Progress")
111
- .option("--dry-run", "Show plan only (no writes, no LLM planning session)")
112
- .option("--non-interactive", "CI: minimal scaffold without prompts (optional --idea)")
113
- .option("--idea <text>", "Short description (CI / smoke only)")
32
+ .description("Create next TASK-NNN, task branch, In Progress (guide-only if one is open)")
33
+ .option("--dry-run", "Plan only")
34
+ .option("--non-interactive", "CI: minimal scaffold (optional --idea)")
35
+ .option("--idea <text>", "Short description (CI)")
114
36
  .option("--cwd <path>", "Target directory")
115
37
  .action(async (opts) => {
116
38
  await taskAddCommand({
@@ -121,87 +43,31 @@ task
121
43
  });
122
44
  });
123
45
  task
124
- .command("status")
125
- .description("Briefing on current TASK progress and recommended next step")
126
- .option("--json", "JSON output")
46
+ .command("done [taskRef]")
47
+ .description("Close TASK: LLM summary, project memory, merge")
48
+ .option("--dry-run", "Plan only")
49
+ .option("--no-merge", "Skip merge")
50
+ .option("--allow-dirty", "Allow dirty tree during merge")
51
+ .option("--yes", "Merge without prompting")
127
52
  .option("--cwd <path>", "Target directory")
128
- .action(async (opts) => {
129
- await taskStatusCommand({
130
- json: Boolean(opts.json),
53
+ .action(async (taskRef, opts) => {
54
+ await taskDoneCommand(taskRef, {
55
+ dryRun: Boolean(opts.dryRun),
56
+ noMerge: Boolean(opts.noMerge),
57
+ allowDirty: Boolean(opts.allowDirty),
58
+ yes: Boolean(opts.yes),
131
59
  cwd: opts.cwd,
132
60
  });
133
61
  });
134
62
  program
135
- .command("rollback [taskRef]")
136
- .description("Advisory rollback for TASK-mvp (--confirm / --confirm-destructive to execute)")
137
- .option("--confirm", "Run non-destructive rollback")
138
- .option("--confirm-destructive", "Allow destructive rollback (reset --hard)")
139
- .option("--strategy <name>", "branch-delete | reset-base | revert-merge")
140
- .option("--keep-branch", "Keep task branch when deleting")
141
- .option("--dry-run", "Print plan only")
142
- .option("--cwd <path>", "Target directory")
143
- .action(async (taskRef, opts) => {
144
- await rollbackCommand(taskRef, opts);
145
- });
146
- const notion = program.command("notion").description("Optional Notion dashboard sync");
147
- notion
148
- .command("init")
149
- .description("Configure Notion in .vibeops.json and .vibeops.env")
150
- .option("--dry-run", "Plan only")
151
- .option("--enable", "Enable Notion")
152
- .option("--projects-db <id>", "Projects database id")
153
- .option("--tasks-db <id>", "Tasks database id")
154
- .option("--non-interactive", "No prompts")
155
- .option("--cwd <path>", "Target directory")
156
- .action(async (opts) => {
157
- await notionInitCommand(opts);
158
- });
159
- notion
160
- .command("test")
161
- .description("Verify Notion token and database schema")
162
- .option("--json", "JSON output")
163
- .option("--debug-shape", "Print API shape diagnostics")
164
- .option("--cwd <path>", "Target directory")
165
- .action(async (opts) => {
166
- await notionTestCommand(opts);
167
- });
168
- notion
169
- .command("sync")
170
- .description("Push docs/project + TASK metadata to Notion")
171
- .option("--dry-run", "Query only")
172
- .option("--json", "JSON output")
173
- .option("--only-tasks", "Tasks DB only")
174
- .option("--only-project", "Project DB only")
175
- .option("--cwd <path>", "Target directory")
176
- .action(async (opts) => {
177
- await notionSyncCommand(opts);
178
- });
179
- const github = program.command("github").description("Optional GitHub remote setup");
180
- github
181
63
  .command("status")
182
- .description("gh auth, remotes, .vibeops.json github section")
64
+ .description("Briefing: active TASK, Git, next hint")
183
65
  .option("--json", "JSON output")
184
66
  .option("--cwd <path>", "Target directory")
185
67
  .action(async (opts) => {
186
- await githubStatusCommand(opts);
187
- });
188
- github
189
- .command("init")
190
- .description("Connect or create a GitHub repo and update config")
191
- .option("--dry-run", "Plan only")
192
- .option("--yes", "Non-interactive defaults")
193
- .option("--owner <owner>", "GitHub owner")
194
- .option("--repo <repo>", "Repo name")
195
- .option("--public", "Public repo")
196
- .option("--private", "Private repo")
197
- .option("--remote <name>", "Remote name (default origin)")
198
- .option("--connect <ownerOrUrl>", "Existing repo")
199
- .option("--no-package-update", "Skip package.json repo fields")
200
- .option("--cwd <path>", "Target directory")
201
- .action(async (opts) => {
202
- await githubInitCommand({
203
- ...opts,
204
- noPackageUpdate: opts.packageUpdate === false,
68
+ await statusCommand({
69
+ json: Boolean(opts.json),
70
+ cwd: opts.cwd,
205
71
  });
206
72
  });
207
73
  program.parseAsync(process.argv).catch((err) => {
@@ -34,9 +34,9 @@ export async function initCommand(options = {}) {
34
34
  if (!dryRun) {
35
35
  log.blank();
36
36
  log.info("Next steps:");
37
- log.info(" 1. Open AGENTS.md and confirm the project name.");
38
- log.info(" 2. Run `vibeops status` to verify installation.");
39
- log.info(" 3. Run `vibeops plan` to populate docs/project/* (or fill them by hand).");
37
+ log.info(" 1. Read AGENTS.md and `.cursor/rules/`.");
38
+ log.info(" 2. Run `vibeops task add` to create the first TASK.");
39
+ log.info(" 3. Plan in Cursor Ask (@docs/tasks/TASK-001-….md), then implement.");
40
40
  }
41
41
  }
42
42
  async function resolveGitPlan(options, interactive) {
@@ -1,12 +1,149 @@
1
1
  import { resolve } from "node:path";
2
- import { collectStatus } from "../status/collector.js";
3
- import { printHuman, toJson } from "../status/format.js";
4
- import { log } from "../lib/logger.js";
5
- export async function statusCommand(options = {}) {
6
- const cwd = resolve(options.cwd ?? process.cwd());
7
- const report = await collectStatus(cwd);
8
- if (options.json) {
9
- log.raw(toJson(report));
2
+ import { readConfig } from "../lib/config.js";
3
+ import { readText } from "../lib/filesystem.js";
4
+ import { readGitInfo } from "../lib/git.js";
5
+ import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
6
+ import { projectPaths } from "../lib/paths.js";
7
+ import { VERSION } from "../version.js";
8
+ import { computeNextHint, hintToText, listInProgressTasks, pickFocusTask, relPath, taskBranchExistsFor, } from "../lib/task-context.js";
9
+ import { countTasks, hasNonEmptySection, isOnTaskBranch, loadActionableTasks, readGitContext, statusDisplay } from "../lib/task.js";
10
+ import { summarizeGoal } from "../lib/task-summary.js";
11
+ async function buildReport(cwd) {
12
+ const paths = projectPaths(cwd);
13
+ const config = await readConfig(cwd);
14
+ const git = await readGitInfo(cwd);
15
+ const isVibeopsProject = config !== null;
16
+ let counts = null;
17
+ let inProgress = [];
18
+ let focus = null;
19
+ if (isVibeopsProject) {
20
+ const tasks = await loadActionableTasks(paths.docsTasks);
21
+ counts = countTasks(tasks);
22
+ inProgress = await listInProgressTasks(paths);
23
+ focus = await pickFocusTask(paths, cwd);
24
+ }
25
+ let focusBlock = null;
26
+ let resultFilled = false;
27
+ let testFilled = false;
28
+ let onTaskBranch = null;
29
+ let taskBranch = null;
30
+ let branchExists = null;
31
+ let goalExcerpt = "";
32
+ if (focus !== null) {
33
+ const body = await readText(focus.filePath);
34
+ goalExcerpt = summarizeGoal(body);
35
+ resultFilled = hasNonEmptySection(body, "Result");
36
+ testFilled = hasNonEmptySection(body, "Test Result");
37
+ const ctx = await readGitContext(focus.filePath);
38
+ taskBranch = ctx?.taskBranch ?? null;
39
+ onTaskBranch = isOnTaskBranch(git.branch, ctx);
40
+ branchExists = await taskBranchExistsFor(cwd, focus);
41
+ focusBlock = {
42
+ id: focus.id,
43
+ title: focus.title,
44
+ status: statusDisplay(focus.status),
45
+ file: relPath(cwd, focus.filePath),
46
+ goalExcerpt,
47
+ resultFilled,
48
+ testResultFilled: testFilled,
49
+ onTaskBranch,
50
+ taskBranch,
51
+ taskBranchExists: branchExists,
52
+ };
53
+ }
54
+ const hint = computeNextHint({
55
+ isVibeopsProject,
56
+ focus,
57
+ resultFilled,
58
+ testFilled,
59
+ onTaskBranch,
60
+ });
61
+ return {
62
+ vibeopsVersion: VERSION,
63
+ isVibeopsProject,
64
+ projectName: config?.name ?? null,
65
+ git: {
66
+ isRepo: git.isRepo,
67
+ branch: git.branch,
68
+ dirty: git.dirty,
69
+ },
70
+ counts,
71
+ inProgress: inProgress.map((t) => ({
72
+ id: t.id,
73
+ title: t.title,
74
+ file: relPath(cwd, t.filePath),
75
+ })),
76
+ focus: focusBlock,
77
+ nextHint: hintToText(hint, cwd, focus),
78
+ };
79
+ }
80
+ function printHuman(report) {
81
+ log.blank();
82
+ log.info(bold("─".repeat(56)));
83
+ log.info(bold(" VibeOps · status"));
84
+ log.info(bold("─".repeat(56)));
85
+ log.info(` ${dim("CLI")} v${report.vibeopsVersion}`);
86
+ if (!report.isVibeopsProject) {
87
+ log.info(` ${dim("Project")} ${yellow("not initialized")} — run vibeops init`);
88
+ log.info(bold("─".repeat(56)));
89
+ log.blank();
90
+ return;
91
+ }
92
+ log.info(` ${dim("Project")} ${report.projectName ?? "(unnamed)"}`);
93
+ if (report.counts) {
94
+ const c = report.counts;
95
+ log.info(` ${dim("Tasks")} ${c.total} total · ${cyan(String(c.in_progress))} in progress · ${c.done} done`);
96
+ }
97
+ if (report.inProgress.length > 1) {
98
+ log.blank();
99
+ log.info(bold(" In progress"));
100
+ for (const t of report.inProgress) {
101
+ log.info(` ${cyan(t.id)} — ${t.title}`);
102
+ }
103
+ }
104
+ log.blank();
105
+ if (report.focus) {
106
+ const f = report.focus;
107
+ log.info(` ${dim("Focus")} ${cyan(f.id)} — ${f.title || dim("(no title)")}`);
108
+ log.info(` ${dim("Status")} ${f.status}`);
109
+ log.info(` ${dim("File")} ${f.file}`);
110
+ if (f.goalExcerpt)
111
+ log.info(` ${dim("Goal")} ${f.goalExcerpt}`);
112
+ log.info(` ${dim("Result")} ${f.resultFilled ? green("filled") : yellow("empty")} · Test Result ${f.testResultFilled ? green("filled") : yellow("empty")}`);
113
+ }
114
+ else {
115
+ log.info(` ${dim("Focus")} ${dim("(none)")}`);
116
+ }
117
+ log.blank();
118
+ log.info(bold(" Git"));
119
+ if (!report.git.isRepo) {
120
+ log.info(` ${yellow("not a git repository")}`);
121
+ }
122
+ else {
123
+ log.info(` ${dim("HEAD")} ${report.git.branch ?? "(detached)"}`);
124
+ if (report.git.dirty === true)
125
+ log.info(` ${dim("Tree")} ${yellow("dirty")}`);
126
+ else if (report.git.dirty === false)
127
+ log.info(` ${dim("Tree")} ${green("clean")}`);
128
+ if (report.focus?.taskBranch) {
129
+ const on = report.focus.onTaskBranch === true
130
+ ? green("yes")
131
+ : report.focus.onTaskBranch === false
132
+ ? yellow("no")
133
+ : dim("—");
134
+ log.info(` ${dim("Task br.")} ${report.focus.taskBranch} · on branch: ${on}`);
135
+ }
136
+ }
137
+ log.blank();
138
+ log.info(`${dim("Next")} ${report.nextHint}`);
139
+ log.info(bold("─".repeat(56)));
140
+ log.blank();
141
+ }
142
+ export async function statusCommand(opts = {}) {
143
+ const cwd = resolve(opts.cwd ?? process.cwd());
144
+ const report = await buildReport(cwd);
145
+ if (opts.json) {
146
+ log.raw(JSON.stringify(report, null, 2));
10
147
  }
11
148
  else {
12
149
  printHuman(report);
@@ -1,200 +1,100 @@
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";
1
+ import { resolve } from "node:path";
5
2
  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";
3
+ import { askInput } from "../lib/inquirer-helpers.js";
4
+ import { bold, cyan, dim, log, yellow } from "../lib/logger.js";
8
5
  import { projectPaths } from "../lib/paths.js";
9
6
  import { readConfig } from "../lib/config.js";
10
7
  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
- }
8
+ import { fallbackTaskDraft, llmScaffoldTask } from "../lib/task-add-llm.js";
9
+ import { allocateNextTaskNumber, formatTaskId, uniqueTaskPath, } from "../lib/task-scaffold.js";
10
+ import { findBlockingTask, relPath } from "../lib/task-context.js";
11
+ import { startTaskBranch } from "../lib/task-start.js";
12
+ import { loadActionableTasks, readGitContext } from "../lib/task.js";
54
13
  export async function taskAddCommand(opts = {}) {
55
14
  const root = resolve(opts.cwd ?? process.cwd());
56
15
  const dryRun = opts.dryRun === true;
57
16
  const nonInteractive = opts.nonInteractive === true || dryRun || process.stdin.isTTY !== true;
58
17
  log.info(bold("vibeops task add"));
59
18
  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);
19
+ const paths = projectPaths(root);
20
+ const blocking = await findBlockingTask(paths, root);
21
+ if (blocking !== null) {
22
+ const ctx = await readGitContext(blocking.filePath);
23
+ log.warn(`${yellow("Blocked")} — ${bold(blocking.id)} is still open (${blocking.title || "no title"}).`);
24
+ if (ctx) {
25
+ log.info(` ${dim("branch")} ${ctx.taskBranch}`);
26
+ }
27
+ log.info(` ${dim("file")} ${relPath(root, blocking.filePath)}`);
28
+ log.blank();
29
+ log.info(`Finish it first: ${cyan(`vibeops task done ${blocking.id}`)}`);
30
+ log.info(`Then run ${cyan("vibeops task add")} again.`);
31
+ process.exitCode = 1;
32
+ return;
67
33
  }
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");
34
+ const ideaDefault = "New work slice";
74
35
  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)",
36
+ message: "What are you doing now? (short)",
78
37
  nonInteractive,
79
- default: ideaDefault,
38
+ default: opts.idea?.trim() || ideaDefault,
80
39
  required: true,
81
40
  });
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);
41
+ const tasks = await loadActionableTasks(paths.docsTasks);
42
+ const taskId = formatTaskId(allocateNextTaskNumber(tasks));
43
+ let title;
44
+ let slug;
45
+ let markdown;
46
+ if (dryRun || nonInteractive) {
47
+ const fb = fallbackTaskDraft(taskId, idea);
48
+ title = fb.title;
49
+ slug = fb.slug;
50
+ markdown = fb.markdown;
120
51
  }
121
52
  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}`;
53
+ const llm = await llmScaffoldTask({ cwd: root, taskId, idea });
54
+ if (llm !== null) {
55
+ title = llm.title;
56
+ slug = llm.slug;
57
+ markdown = llm.markdown;
58
+ log.skip(`Scaffold via ${llm.provider}`);
136
59
  }
137
60
  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
- }
61
+ log.warn("LLM unavailable — using minimal TASK template.");
62
+ const fb = fallbackTaskDraft(taskId, idea);
63
+ title = fb.title;
64
+ slug = fb.slug;
65
+ markdown = fb.markdown;
149
66
  }
150
67
  }
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
- }
68
+ const { filePath } = uniqueTaskPath(paths.docsTasks, taskId, slugify(slug || title), tasks.map((t) => t.filePath));
69
+ const relFile = relPath(root, filePath);
159
70
  if (dryRun) {
160
71
  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();
72
+ log.info(dim(` branch task/${slugify(slug || title).replace(/^(\d+)-/, "$1-")}`));
165
73
  return;
166
74
  }
167
75
  if (await pathExists(filePath)) {
168
76
  throw new Error(`TASK file already exists: ${relFile}`);
169
77
  }
170
78
  await writeText(filePath, markdown);
171
- await mkdir(join(root, ".vibeops", "generated"), { recursive: true });
172
- await writeText(buildAbs, buildPromptMarkdown);
173
79
  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;
80
+ const config = await readConfig(root);
81
+ if (config === null) {
82
+ log.warn("No .vibeops.json — run vibeops init for full template.");
183
83
  }
184
84
  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.`);
85
+ log.step("Starting task branch…");
86
+ const started = await startTaskBranch({
87
+ cwd: root,
88
+ taskFile: filePath,
89
+ allowDirty: true,
90
+ });
91
+ if (!started) {
92
+ process.exitCode = 1;
189
93
  return;
190
94
  }
191
95
  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
- }
96
+ log.info(bold("Next in Cursor"));
97
+ log.info(` Ask: @${relFile} — plan Scope / Acceptance Criteria`);
98
+ log.info(` Agent: same file — implement`);
99
+ log.info(` Done: ${cyan(`vibeops task done ${taskId}`)}`);
200
100
  }