@goodtek/vibeops 0.4.0 → 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 (106) hide show
  1. package/CHANGELOG.md +198 -1
  2. package/README.md +62 -402
  3. package/dist/bootstrap/installer.js +4 -16
  4. package/dist/cli.js +52 -244
  5. package/dist/commands/init.js +3 -3
  6. package/dist/commands/status.js +145 -8
  7. package/dist/commands/task-add.js +100 -0
  8. package/dist/commands/task-done.js +117 -74
  9. package/dist/lib/config.js +2 -118
  10. package/dist/lib/git.js +190 -8
  11. package/dist/lib/llm-complete.js +62 -0
  12. package/dist/lib/paths.js +7 -3
  13. package/dist/lib/plan-llm-openai.js +0 -63
  14. package/dist/lib/resolve-task.js +34 -0
  15. package/dist/lib/slug.js +11 -0
  16. package/dist/lib/task-add-llm.js +81 -0
  17. package/dist/lib/task-context.js +68 -0
  18. package/dist/lib/task-done-llm.js +148 -0
  19. package/dist/lib/task-merge.js +115 -0
  20. package/dist/lib/task-scaffold.js +70 -92
  21. package/dist/lib/task-start.js +72 -0
  22. package/dist/lib/task-summary.js +12 -239
  23. package/dist/lib/task.js +80 -37
  24. package/dist/types/config.js +0 -16
  25. package/package.json +3 -4
  26. package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  27. package/templates/.cursor/rules/02-git-safety.mdc +11 -0
  28. package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
  29. package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
  30. package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
  31. package/templates/AGENTS.md +26 -87
  32. package/templates/docs/logs/README.md +4 -35
  33. package/templates/docs/project/03-architecture.md +10 -29
  34. package/templates/docs/project/05-current-state.md +7 -27
  35. package/templates/docs/project/06-decisions.md +6 -17
  36. package/templates/docs/tasks/TASK-000-template.md +9 -44
  37. package/dist/agent/loader.js +0 -71
  38. package/dist/agent/prompt.js +0 -66
  39. package/dist/commands/agent-list.js +0 -32
  40. package/dist/commands/agent-prompt.js +0 -59
  41. package/dist/commands/agent-show.js +0 -26
  42. package/dist/commands/github-init.js +0 -554
  43. package/dist/commands/github-status.js +0 -164
  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 -343
  48. package/dist/commands/task-check.js +0 -155
  49. package/dist/commands/task-generate.js +0 -206
  50. package/dist/commands/task-pull.js +0 -277
  51. package/dist/commands/task-rollback.js +0 -174
  52. package/dist/commands/task-start.js +0 -102
  53. package/dist/lib/brief.js +0 -356
  54. package/dist/lib/github-cli.js +0 -247
  55. package/dist/lib/notion-client.js +0 -459
  56. package/dist/lib/notion-discovery.js +0 -671
  57. package/dist/lib/notion-env.js +0 -140
  58. package/dist/lib/notion-mappers.js +0 -148
  59. package/dist/lib/notion-schema.js +0 -272
  60. package/dist/lib/notion-sync.js +0 -337
  61. package/dist/lib/notion-target.js +0 -247
  62. package/dist/lib/package-json.js +0 -133
  63. package/dist/lib/plan-apply-planner.js +0 -131
  64. package/dist/lib/plan-codex-pkce.js +0 -234
  65. package/dist/lib/plan-llm-models.js +0 -237
  66. package/dist/lib/plan-llm-normalize.js +0 -146
  67. package/dist/lib/plan-llm-session.js +0 -682
  68. package/dist/lib/plan-post-apply-setup.js +0 -116
  69. package/dist/lib/project-docs.js +0 -95
  70. package/dist/lib/prompt-builder.js +0 -135
  71. package/dist/lib/task-generator.js +0 -183
  72. package/dist/lib/task-prompt.js +0 -23
  73. package/dist/lib/task-pull.js +0 -354
  74. package/dist/status/collector.js +0 -103
  75. package/dist/status/format.js +0 -177
  76. package/dist/types/brief.js +0 -126
  77. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  78. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  79. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  80. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  81. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  82. package/templates/.vibeops/agents/architect.md +0 -47
  83. package/templates/.vibeops/agents/builder.md +0 -38
  84. package/templates/.vibeops/agents/docs.md +0 -54
  85. package/templates/.vibeops/agents/orchestrator.md +0 -40
  86. package/templates/.vibeops/agents/planner.md +0 -60
  87. package/templates/.vibeops/agents/recovery.md +0 -49
  88. package/templates/.vibeops/agents/reviewer.md +0 -47
  89. package/templates/.vibeops/agents/tester.md +0 -43
  90. package/templates/.vibeops/prompts/create-plan.md +0 -33
  91. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  92. package/templates/.vibeops/prompts/implement-task.md +0 -39
  93. package/templates/.vibeops/prompts/review-task.md +0 -34
  94. package/templates/.vibeops/prompts/rollback.md +0 -32
  95. package/templates/.vibeops/prompts/start-project.md +0 -39
  96. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  97. package/templates/.vibeops/workflows/project-start.md +0 -73
  98. package/templates/.vibeops/workflows/rollback.md +0 -45
  99. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  100. package/templates/docs/project/00-overview.md +0 -27
  101. package/templates/docs/project/01-requirements.md +0 -30
  102. package/templates/docs/project/02-mvp-scope.md +0 -36
  103. package/templates/docs/project/04-tech-stack.md +0 -29
  104. package/templates/docs/project/07-backlog.md +0 -23
  105. package/templates/docs/project/08-env.md +0 -29
  106. package/templates/docs/project/09-deployment.md +0 -28
package/dist/cli.js CHANGED
@@ -1,265 +1,73 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { agentListCommand } from "./commands/agent-list.js";
4
- import { agentPromptCommand } from "./commands/agent-prompt.js";
5
- import { agentShowCommand } from "./commands/agent-show.js";
6
- import { githubInitCommand } from "./commands/github-init.js";
7
- import { githubStatusCommand } from "./commands/github-status.js";
8
3
  import { initCommand } from "./commands/init.js";
9
- import { notionInitCommand } from "./commands/notion-init.js";
10
- import { notionSyncCommand } from "./commands/notion-sync.js";
11
- import { notionTestCommand } from "./commands/notion-test.js";
12
- import { planCommand } from "./commands/plan.js";
13
4
  import { statusCommand } from "./commands/status.js";
14
- import { taskCheckCommand } from "./commands/task-check.js";
5
+ import { taskAddCommand } from "./commands/task-add.js";
15
6
  import { taskDoneCommand } from "./commands/task-done.js";
16
- import { taskGenerateCommand } from "./commands/task-generate.js";
17
- import { taskPullCommand } from "./commands/task-pull.js";
18
- import { taskRollbackCommand } from "./commands/task-rollback.js";
19
- import { taskStartCommand } from "./commands/task-start.js";
20
7
  import { VERSION } from "./version.js";
21
8
  const program = new Command();
22
9
  program
23
10
  .name("vibeops")
24
- .description("VibeOps — a local CLI that keeps Cursor-based vibe coding on rails.\n" +
25
- " It installs docs, Cursor rules, AGENTS.md, agents, TASK templates,\n" +
26
- " and Git/Notion workflows into a project, then drives work one TASK at a time.")
11
+ .description("VibeOps v3 — Cursor-friendly TASK workflow (init · task add · task done · status)")
27
12
  .version(VERSION, "-v, --version", "Print the VibeOps version");
28
13
  program
29
14
  .command("init")
30
- .description("Install the VibeOps workflow files into the current directory")
31
- .option("--dry-run", "Show what would be created without writing any files")
32
- .option("--force", "Overwrite existing files (use with care)")
33
- .option("--cwd <path>", "Run against a different directory")
34
- .option("--name <projectName>", "Project name written into .vibeops.json")
35
- .option("--git", "Initialize a Git repository without prompting")
36
- .option("--no-git", "Skip Git initialization and commits")
37
- .option("--initial-commit", "Run `git add .` and create an initial commit")
38
- .option("--no-initial-commit", "Do not create an initial commit")
39
- .option("--default-branch <name>", "Default Git branch name (default `main`)")
40
- .option("--commit-message <message>", "Initial commit message (default 'chore: initialize vibeops project')")
41
- .action(async (options) => {
42
- await initCommand(options);
43
- });
44
- program
45
- .command("status")
46
- .description("Show VibeOps installation, TASK counts, and integration state")
47
- .option("--json", "Print machine-readable JSON")
48
- .option("--cwd <path>", "Inspect a different directory")
49
- .action(async (options) => {
50
- await statusCommand(options);
51
- });
52
- program
53
- .command("plan")
54
- .description("Requires `vibeops init`: LLM planning → ProjectBrief + planning prompt; optional --apply-planner runs the planner via the same LLM (docs + TASKs, commit, push, Notion). Use --legacy-wizard for the fixed 20-question flow")
55
- .option("--idea <text>", "One-line idea default (use `Name: idea` to extract the project name)")
56
- .option("--from <path>", "Read an existing brief markdown and regenerate the prompt")
57
- .option("--output <path>", "Output path for the Cursor planning prompt (default `.vibeops/generated/plan-prompt.md`)")
58
- .option("--non-interactive", "Skip prompts and use the supplied values plus safe placeholders (no LLM)")
59
- .option("--legacy-wizard", "Use the original 20-question wizard instead of an LLM planning session")
60
- .option("--provider <id>", "LLM provider when several are available: openai | codex-oauth | cursor-agent")
61
- .option("--model <id>", "LLM model id for the selected provider (skips interactive model list; same as setting VIBEOPS_*_MODEL for that path)")
62
- .option("--cwd <path>", "Run against a different directory")
63
- .option("--apply-planner", "After writing the brief + prompt: call the same LLM provider to emit docs/project + docs/tasks; then (TTY) ask commit → push → Notion in order, or (--non-interactive) commit unless --no-git-commit, push only with --push, Notion unless --no-notion-sync")
64
- .option("--apply-dry-run", "With --apply-planner (or alone): call the LLM and list files that would be written; no writes, git, or Notion")
65
- .option("--no-git-commit", "With --apply-planner: skip git commit after writes")
66
- .option("--push", "With --apply-planner: run git push after commit (TTY: optional `vibeops github init` before push; skips the push Yes/No prompt)")
67
- .option("--no-notion-sync", "With --apply-planner: skip Notion sync even when the brief enables it")
68
- .action(async (options) => {
69
- const provider = options.provider === "openai" ||
70
- options.provider === "codex-oauth" ||
71
- options.provider === "cursor-agent"
72
- ? options.provider
73
- : undefined;
74
- await planCommand({
75
- idea: options.idea,
76
- from: options.from,
77
- output: options.output,
78
- nonInteractive: options.nonInteractive,
79
- legacyWizard: options.legacyWizard === true,
80
- provider,
81
- llmModel: typeof options.model === "string" ? options.model : undefined,
82
- cwd: options.cwd,
83
- applyPlanner: options.applyPlanner === true,
84
- applyPlannerDryRun: options.applyDryRun === true,
85
- noGitCommit: options.noGitCommit === true,
86
- gitPush: options.push === true,
87
- noNotionSync: options.noNotionSync === true,
88
- });
89
- });
90
- const agent = program
91
- .command("agent")
92
- .description("Inspect the `.vibeops/agents/*` agent definitions");
93
- agent
94
- .command("list")
95
- .description("List available agents")
96
- .option("--json", "Print machine-readable JSON")
97
- .option("--cwd <path>", "Inspect a different directory")
98
- .action(async (options) => {
99
- await agentListCommand(options);
100
- });
101
- agent
102
- .command("show <name>")
103
- .description("Print an agent definition body")
104
- .option("--raw", "Include frontmatter in the output")
105
- .option("--cwd <path>", "Inspect a different directory")
106
- .action(async (name, options) => {
107
- await agentShowCommand(name, options);
108
- });
109
- agent
110
- .command("prompt <name> <taskId>")
111
- .description("Print a Cursor-ready prompt built from the agent + TASK context")
112
- .option("--context <path...>", "Additional context file paths")
113
- .option("--cwd <path>", "Inspect a different directory")
114
- .action(async (name, taskId, options) => {
115
- await agentPromptCommand(name, taskId, options);
116
- });
117
- const task = program
118
- .command("task")
119
- .description("TASK lifecycle commands");
120
- task
121
- .command("generate")
122
- .description("Build a Cursor prompt for generating TASK files, or with --scaffold write skeleton TASK markdown")
123
- .option("--from <path>", "Primary backlog/brief markdown to feed into the prompt")
124
- .option("--output <path>", "Output path for the generated prompt (default `.vibeops/generated/task-generate-prompt.md`)")
125
- .option("--count <number>", "Suggested TASK count for Cursor (default 8, warns above 20)")
126
- .option("--phase <name>", "Generate TASKs for a specific phase label only (e.g. 'MVP 4')")
127
- .option("--scaffold", "Write skeleton TASK markdown files directly, without an LLM")
128
- .option("--dry-run", "Print the plan without writing or modifying files")
129
- .option("--cwd <path>", "Run against a different directory")
130
- .action(async (options) => {
131
- await taskGenerateCommand(options);
132
- });
133
- task
134
- .command("start <taskId>")
135
- .description("Confirm a clean working tree (or docs-only governance changes), create the task branch, record Status/Git context, and print a Builder prompt")
136
- .option("--dry-run", "Print the plan without touching files or Git")
137
- .option("--allow-dirty", "Proceed even if the Git working tree is dirty (including non-doc changes)")
138
- .option("--agent <name>", "Agent to build the prompt with (default `builder`)")
139
- .option("--cwd <path>", "Run against a different directory")
140
- .action(async (taskId, options) => {
141
- await taskStartCommand(taskId, options);
142
- });
15
+ .description("Install the VibeOps v3 project tree")
16
+ .option("--dry-run", "Show what would be created")
17
+ .option("--force", "Overwrite existing files")
18
+ .option("--cwd <path>", "Target directory")
19
+ .option("--name <projectName>", "Project name in .vibeops.json")
20
+ .option("--git", "Initialize Git")
21
+ .option("--no-git", "Skip Git")
22
+ .option("--initial-commit", "Create initial commit")
23
+ .option("--no-initial-commit", "Skip initial commit")
24
+ .option("--default-branch <name>", "Default branch (default main)")
25
+ .option("--commit-message <message>", "Initial commit message")
26
+ .action(async (opts) => {
27
+ await initCommand(opts);
28
+ });
29
+ const task = program.command("task").description("TASK lifecycle");
143
30
  task
144
- .command("prompt <taskId>")
145
- .description("Print a Cursor-ready prompt built from the TASK + agent context")
146
- .option("--agent <name>", "Agent name (orchestrator / planner / architect / builder / reviewer / tester / docs / recovery)")
147
- .option("--context <path...>", "Additional context file paths")
148
- .option("--cwd <path>", "Inspect a different directory")
149
- .action(async (taskId, options) => {
150
- await agentPromptCommand(options.agent ?? "builder", taskId, {
151
- cwd: options.cwd,
152
- context: options.context,
31
+ .command("add")
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)")
36
+ .option("--cwd <path>", "Target directory")
37
+ .action(async (opts) => {
38
+ await taskAddCommand({
39
+ dryRun: Boolean(opts.dryRun),
40
+ nonInteractive: Boolean(opts.nonInteractive),
41
+ idea: opts.idea,
42
+ cwd: opts.cwd,
153
43
  });
154
44
  });
155
45
  task
156
- .command("check <taskId>")
157
- .description("Read-only check: git diff/log + acceptance criteria + doc updates + Result fields + a Reviewer prompt")
158
- .option("--strict", "Exit with code 1 if any required item is missing")
159
- .option("--agent <name>", "Agent to build the prompt with (default `reviewer`)")
160
- .option("--cwd <path>", "Inspect a different directory")
161
- .action(async (taskId, options) => {
162
- await taskCheckCommand(taskId, options);
163
- });
164
- task
165
- .command("done <taskId>")
166
- .description("Validate Result/Test Result, move Status to Review, and print a commit message (no auto-commit)")
167
- .option("--dry-run", "Print the plan without touching files")
168
- .option("--finalize", "Move Status to Done instead of Review (use after human review)")
169
- .option("--cwd <path>", "Run against a different directory")
170
- .action(async (taskId, options) => {
171
- await taskDoneCommand(taskId, options);
172
- });
173
- task
174
- .command("rollback <taskId>")
175
- .description("Default: advisory only. --confirm: non-destructive rollback. --confirm-destructive: hard reset.")
176
- .option("--confirm", "Allow non-destructive rollback execution (branch-delete etc.)")
177
- .option("--confirm-destructive", "Allow destructive rollback execution (reset --hard etc.)")
178
- .option("--strategy <name>", "branch-delete | reset-base | revert-merge (default branch-delete)")
179
- .option("--keep-branch", "Keep the task branch even when using branch-delete")
180
- .option("--dry-run", "Print the plan without running any git commands, even with --confirm")
181
- .option("--cwd <path>", "Run against a different directory")
182
- .action(async (taskId, options) => {
183
- await taskRollbackCommand(taskId, options);
184
- });
185
- task
186
- .command("pull")
187
- .description("Generate `docs/tasks/*.md` skeletons from Notion Tasks DB rows (defaults to Status = Planned)")
188
- .option("--dry-run", "Print the plan without touching files or Notion")
189
- .option("--json", "Print machine-readable JSON")
190
- .option("--status <name>", "Notion Status values to pull (comma-separated, e.g. 'Planned,Ready'). Default `Planned`")
191
- .option("--limit <number>", "Maximum rows to pull from Notion (default 20, max 100)")
192
- .option("--cwd <path>", "Run against a different directory")
193
- .option("--verbose", "Print a decision trace per considered row (taskId / pageId / docsPath / reason)")
194
- .action(async (options) => {
195
- await taskPullCommand(options);
196
- });
197
- const notion = program
198
- .command("notion")
199
- .description("Notion dashboard sync");
200
- notion
201
- .command("init")
202
- .description("Interactive setup: use arrow keys + Enter to pick Yes/No, then write `.vibeops.json` `notion` section and `.vibeops.env(.example)`")
203
- .option("--dry-run", "Print the plan without changing files (no interactive prompts)")
204
- .option("--enable", "Set `notion.enabled = true` and skip the first prompt")
205
- .option("--projects-db <id>", "Set `notion.projectsDatabaseId` (skip the prompt)")
206
- .option("--tasks-db <id>", "Set `notion.tasksDatabaseId` (skip the prompt)")
207
- .option("--non-interactive", "Force non-interactive mode in a TTY (use flag values + safe defaults only)")
208
- .option("--cwd <path>", "Run against a different directory")
209
- .action(async (options) => {
210
- await notionInitCommand(options);
211
- });
212
- notion
213
- .command("test")
214
- .description("Read-only check: Notion API auth + access to Projects/Tasks DBs + required-property schema validation")
215
- .option("--json", "Print machine-readable JSON")
216
- .option("--debug-shape", "Also print a token-safe diagnostic of the Projects/Tasks retrieve responses (top-level keys, data_sources, etc.)")
217
- .option("--cwd <path>", "Run against a different directory")
218
- .action(async (options) => {
219
- await notionTestCommand(options);
220
- });
221
- notion
222
- .command("sync")
223
- .description("Push `docs/project` + `docs/tasks` metadata into Notion Projects/Tasks DBs (read-only on local files)")
224
- .option("--dry-run", "Print the plan without any Notion mutation (queries only)")
225
- .option("--json", "Print machine-readable JSON")
226
- .option("--only-tasks", "Sync the Tasks DB only (leave the Project row alone)")
227
- .option("--only-project", "Sync the Project DB only (leave Task rows alone)")
228
- .option("--cwd <path>", "Run against a different directory")
229
- .action(async (options) => {
230
- await notionSyncCommand(options);
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")
52
+ .option("--cwd <path>", "Target directory")
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),
59
+ cwd: opts.cwd,
60
+ });
231
61
  });
232
- const github = program
233
- .command("github")
234
- .description("GitHub repository integration");
235
- github
62
+ program
236
63
  .command("status")
237
- .description("Read-only check: gh install/auth + git remotes + `.vibeops.json` github section + `package.json` repo fields")
238
- .option("--json", "Print machine-readable JSON")
239
- .option("--cwd <path>", "Inspect a different directory")
240
- .action(async (options) => {
241
- await githubStatusCommand(options);
242
- });
243
- github
244
- .command("init")
245
- .description("Interactive setup: check gh auth, manage the git remote, optionally `gh repo create`, then update `.vibeops.json` and `package.json`")
246
- .option("--dry-run", "Print the plan without running gh / git or writing files")
247
- .option("--yes", "Skip interactive prompts and use safe defaults")
248
- .option("--owner <owner>", "GitHub owner (user or org)")
249
- .option("--repo <repo>", "GitHub repo name")
250
- .option("--public", "Force visibility = public (skip the prompt)")
251
- .option("--private", "Force visibility = private (skip the prompt)")
252
- .option("--remote <name>", "Git remote name (default `origin`)")
253
- .option("--connect <ownerOrUrl>", "Connect to an existing repo instead of creating one (owner/repo or https/ssh URL)")
254
- .option("--no-package-update", "Do not modify `package.json` repository/homepage/bugs fields")
255
- .option("--cwd <path>", "Run against a different directory")
256
- .action(async (options) => {
257
- await githubInitCommand({
258
- ...options,
259
- // Commander turns `--no-package-update` into `packageUpdate: false`.
260
- // Our command type still accepts the legacy `noPackageUpdate`
261
- // shape for tests / direct invocation.
262
- noPackageUpdate: options.packageUpdate === false,
64
+ .description("Briefing: active TASK, Git, next hint")
65
+ .option("--json", "JSON output")
66
+ .option("--cwd <path>", "Target directory")
67
+ .action(async (opts) => {
68
+ await statusCommand({
69
+ json: Boolean(opts.json),
70
+ cwd: opts.cwd,
263
71
  });
264
72
  });
265
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);
@@ -0,0 +1,100 @@
1
+ import { resolve } from "node:path";
2
+ import { pathExists, writeText } from "../lib/filesystem.js";
3
+ import { askInput } from "../lib/inquirer-helpers.js";
4
+ import { bold, cyan, dim, log, yellow } from "../lib/logger.js";
5
+ import { projectPaths } from "../lib/paths.js";
6
+ import { readConfig } from "../lib/config.js";
7
+ import { slugify } from "../lib/slug.js";
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";
13
+ export async function taskAddCommand(opts = {}) {
14
+ const root = resolve(opts.cwd ?? process.cwd());
15
+ const dryRun = opts.dryRun === true;
16
+ const nonInteractive = opts.nonInteractive === true || dryRun || process.stdin.isTTY !== true;
17
+ log.info(bold("vibeops task add"));
18
+ log.blank();
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;
33
+ }
34
+ const ideaDefault = "New work slice";
35
+ const idea = await askInput({
36
+ message: "What are you doing now? (short)",
37
+ nonInteractive,
38
+ default: opts.idea?.trim() || ideaDefault,
39
+ required: true,
40
+ });
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;
51
+ }
52
+ else {
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}`);
59
+ }
60
+ else {
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;
66
+ }
67
+ }
68
+ const { filePath } = uniqueTaskPath(paths.docsTasks, taskId, slugify(slug || title), tasks.map((t) => t.filePath));
69
+ const relFile = relPath(root, filePath);
70
+ if (dryRun) {
71
+ log.info(`[dry-run] Would create ${bold(taskId)} → ${cyan(relFile)}`);
72
+ log.info(dim(` branch task/${slugify(slug || title).replace(/^(\d+)-/, "$1-")}`));
73
+ return;
74
+ }
75
+ if (await pathExists(filePath)) {
76
+ throw new Error(`TASK file already exists: ${relFile}`);
77
+ }
78
+ await writeText(filePath, markdown);
79
+ log.ok(`Created ${bold(taskId)} → ${cyan(relFile)}`);
80
+ const config = await readConfig(root);
81
+ if (config === null) {
82
+ log.warn("No .vibeops.json — run vibeops init for full template.");
83
+ }
84
+ log.blank();
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;
93
+ return;
94
+ }
95
+ log.blank();
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}`)}`);
100
+ }