@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
@@ -1,343 +0,0 @@
1
- import { isAbsolute, join, relative, resolve } from "node:path";
2
- import { briefToMarkdown, findMissingRequired, gatherBrief, parseBriefFromMarkdown, parseIdea, } from "../lib/brief.js";
3
- import { askYesNo } from "../lib/inquirer-helpers.js";
4
- import { pathExists, readText, writeText } from "../lib/filesystem.js";
5
- import { bold, cyan, dim, log, yellow } from "../lib/logger.js";
6
- import { applyPlannerMarkdownFlow, commitPlannerGit, pushPlannerGit } from "../lib/plan-apply-planner.js";
7
- import { gatherBriefViaLlm, pickPlanDialogueLocale, pickPlanLlmProvider } from "../lib/plan-llm-session.js";
8
- import { pickPlanExecutionModel } from "../lib/plan-llm-models.js";
9
- import { ensureGitForPlannerCommit, offerGithubInitBeforePush, runNotionSyncWithOptionalInit } from "../lib/plan-post-apply-setup.js";
10
- import { buildPlanPrompt } from "../lib/prompt-builder.js";
11
- import { projectPaths } from "../lib/paths.js";
12
- const DEFAULT_BRIEF_REL = ".vibeops/brief/project-brief.md";
13
- const DEFAULT_PROMPT_REL = ".vibeops/generated/plan-prompt.md";
14
- function toAbsolute(root, candidate) {
15
- return isAbsolute(candidate) ? candidate : resolve(root, candidate);
16
- }
17
- function relDisplay(root, abs) {
18
- const r = relative(root, abs);
19
- return r.length === 0 ? "." : r;
20
- }
21
- export async function planCommand(options) {
22
- const cwd = resolve(options.cwd ?? process.cwd());
23
- const nonInteractive = options.nonInteractive === true;
24
- const paths = projectPaths(cwd);
25
- if (!(await pathExists(paths.config))) {
26
- log.error(`Not a VibeOps project: ${cyan(".vibeops.json")} was not found under this directory.`);
27
- log.info(` ${dim("Run")} ${cyan("vibeops init")} ${dim("here first, then run")} ${cyan("vibeops plan")} ${dim("again.")}`);
28
- process.exitCode = 1;
29
- return;
30
- }
31
- const briefAbs = join(cwd, DEFAULT_BRIEF_REL);
32
- const promptAbs = typeof options.output === "string" ? toAbsolute(cwd, options.output) : join(cwd, DEFAULT_PROMPT_REL);
33
- log.info(bold(`vibeops plan`));
34
- log.info(dim(` cwd: ${cwd}`));
35
- if (options.from)
36
- log.info(dim(` from: ${options.from}`));
37
- if (options.idea)
38
- log.info(dim(` idea: ${options.idea}`));
39
- if (typeof options.llmModel === "string" && options.llmModel.trim().length > 0) {
40
- log.info(dim(` model: ${options.llmModel.trim()}`));
41
- }
42
- if (nonInteractive)
43
- log.info(dim(` mode: non-interactive`));
44
- log.blank();
45
- let bundle;
46
- let llmProviderForApply;
47
- let llmModelForApply;
48
- if (typeof options.from === "string" && options.from.length > 0) {
49
- bundle = await loadFromFile({
50
- cwd,
51
- fromPath: toAbsolute(cwd, options.from),
52
- nonInteractive,
53
- idea: options.idea,
54
- });
55
- }
56
- else {
57
- if (!nonInteractive && process.stdin.isTTY !== true) {
58
- log.error("vibeops plan requires a TTY. In CI/piped environments, pass --non-interactive or supply --from <brief.md>.");
59
- process.exitCode = 1;
60
- return;
61
- }
62
- if (nonInteractive) {
63
- log.step("non-interactive: build the ProjectBrief from flag values + safe placeholders");
64
- log.blank();
65
- bundle = await gatherBrief({
66
- cwd,
67
- idea: options.idea,
68
- nonInteractive: true,
69
- });
70
- }
71
- else if (options.legacyWizard === true) {
72
- log.step("legacy wizard: build the ProjectBrief from 20 fixed questions (arrow keys · Space · Enter)");
73
- log.blank();
74
- bundle = await gatherBrief({
75
- cwd,
76
- idea: options.idea,
77
- nonInteractive: false,
78
- legacyWizard: true,
79
- });
80
- }
81
- else {
82
- log.step("LLM planning: verify provider, then interactive discovery + structured brief");
83
- log.blank();
84
- const interactiveLogin = process.stdin.isTTY === true;
85
- const provider = await pickPlanLlmProvider(cwd, options.provider, { interactiveLogin });
86
- if (provider === null) {
87
- return;
88
- }
89
- const forcedModel = typeof options.llmModel === "string" && options.llmModel.trim().length > 0
90
- ? options.llmModel.trim()
91
- : undefined;
92
- const pickedModel = forcedModel ??
93
- (await pickPlanExecutionModel({
94
- provider,
95
- cwd,
96
- interactive: interactiveLogin,
97
- }));
98
- log.blank();
99
- const planningLocale = await pickPlanDialogueLocale();
100
- try {
101
- bundle = await gatherBriefViaLlm({
102
- cwd,
103
- idea: options.idea,
104
- provider,
105
- allowCodexBrowserLogin: interactiveLogin,
106
- llmModelOverride: pickedModel,
107
- planningLocale,
108
- });
109
- llmProviderForApply = provider;
110
- llmModelForApply =
111
- typeof pickedModel === "string" && pickedModel.trim().length > 0 ? pickedModel.trim() : undefined;
112
- }
113
- catch (e) {
114
- const msg = e instanceof Error ? e.message : String(e);
115
- log.error(`LLM planning failed: ${msg}`);
116
- process.exitCode = 1;
117
- return;
118
- }
119
- }
120
- }
121
- const briefMd = briefToMarkdown(bundle.brief, bundle.meta);
122
- await writeText(briefAbs, briefMd);
123
- log.ok(`Wrote brief: ${relDisplay(cwd, briefAbs)}`);
124
- const promptMd = buildPlanPrompt({
125
- brief: bundle.brief,
126
- meta: bundle.meta,
127
- briefRelativePath: relDisplay(cwd, briefAbs),
128
- });
129
- await writeText(promptAbs, promptMd);
130
- log.ok(`Cursor planning prompt: ${relDisplay(cwd, promptAbs)}`);
131
- if (bundle.meta.assumptions.length > 0) {
132
- log.blank();
133
- log.info(`${yellow("!")} ${bold("Assumptions")} (items the Planner Agent should reconfirm):`);
134
- for (const a of bundle.meta.assumptions)
135
- log.info(` · ${a}`);
136
- }
137
- const wantsApply = options.applyPlanner === true || options.applyPlannerDryRun === true;
138
- if (wantsApply) {
139
- const applyDryRun = options.applyPlannerDryRun === true;
140
- const exec = await resolvePlannerApplyExecution({
141
- cwd,
142
- nonInteractive,
143
- fromLlmProvider: llmProviderForApply,
144
- fromLlmModel: llmModelForApply,
145
- cliProvider: options.provider,
146
- cliModel: options.llmModel,
147
- });
148
- if (exec === null) {
149
- return;
150
- }
151
- try {
152
- const applyResult = await applyPlannerMarkdownFlow({
153
- cwd,
154
- planPromptMarkdown: promptMd,
155
- provider: exec.provider,
156
- model: exec.model,
157
- dryRun: applyDryRun,
158
- });
159
- const applyInteractive = !nonInteractive && process.stdin.isTTY === true && !applyDryRun;
160
- const hasWrittenFiles = !applyDryRun && applyResult.writtenRelativePaths.length > 0;
161
- let doCommit = hasWrittenFiles && options.noGitCommit !== true;
162
- if (doCommit && applyInteractive) {
163
- log.blank();
164
- log.step("Next: commit, push, and Notion (confirm each step)");
165
- doCommit = await askYesNo({
166
- message: "[1] Create a git commit for the planner output (docs/project + docs/tasks)?",
167
- nonInteractive: false,
168
- defaultValue: true,
169
- });
170
- }
171
- let committedOk = false;
172
- if (doCommit) {
173
- const gitReady = await ensureGitForPlannerCommit({
174
- cwd,
175
- interactive: applyInteractive,
176
- });
177
- if (!gitReady) {
178
- doCommit = false;
179
- }
180
- }
181
- if (doCommit) {
182
- try {
183
- await commitPlannerGit(cwd, applyResult.writtenRelativePaths, "docs: planner output from vibeops plan (--apply-planner)");
184
- log.ok("Git commit created for planner docs/tasks.");
185
- committedOk = true;
186
- }
187
- catch (e) {
188
- const msg = e instanceof Error ? e.message : String(e);
189
- log.warn(`Git commit did not run (${msg}). Review changes and commit manually.`);
190
- }
191
- }
192
- let doPush = false;
193
- if (committedOk) {
194
- if (options.gitPush === true) {
195
- doPush = true;
196
- }
197
- else if (applyInteractive) {
198
- doPush = await askYesNo({
199
- message: "[2] Push this commit to the remote (git push)? (If Yes, you may be offered `vibeops github init` to connect or create a GitHub repo / set `origin`.)",
200
- nonInteractive: false,
201
- defaultValue: false,
202
- });
203
- }
204
- if (doPush) {
205
- await offerGithubInitBeforePush({ cwd, interactive: applyInteractive });
206
- try {
207
- await pushPlannerGit(cwd);
208
- log.ok("git push complete.");
209
- }
210
- catch (e) {
211
- const msg = e instanceof Error ? e.message : String(e);
212
- log.error(`git push failed: ${msg}`);
213
- process.exitCode = 1;
214
- }
215
- }
216
- }
217
- else if (options.gitPush === true && !committedOk) {
218
- log.warn("`--push` skipped (no successful commit: dry-run, --no-git-commit, you declined the commit prompt, commit error, or no files written).");
219
- }
220
- let doNotion = bundle.brief.useNotion === true && options.noNotionSync !== true && !applyDryRun;
221
- if (doNotion && applyInteractive) {
222
- doNotion = await askYesNo({
223
- message: "[3] Run Notion sync now (project + TASK metadata)?",
224
- nonInteractive: false,
225
- defaultValue: true,
226
- });
227
- }
228
- if (doNotion) {
229
- const hadErrBeforeNotion = process.exitCode === 1;
230
- await runNotionSyncWithOptionalInit({ cwd, interactive: applyInteractive });
231
- if (hadErrBeforeNotion) {
232
- process.exitCode = 1;
233
- }
234
- if (process.exitCode === 1 && !hadErrBeforeNotion) {
235
- log.warn("Notion sync failed — fix integration and run `vibeops notion sync`.");
236
- }
237
- }
238
- else if (bundle.brief.useNotion === true && applyDryRun) {
239
- log.info(dim("Notion sync skipped for --apply-dry-run."));
240
- }
241
- else if (bundle.brief.useNotion === true && options.noNotionSync === true) {
242
- log.info(dim("Notion sync skipped (--no-notion-sync)."));
243
- }
244
- else if (bundle.brief.useNotion === true && applyInteractive && !doNotion) {
245
- log.info(dim("Notion sync skipped (you chose No)."));
246
- }
247
- }
248
- catch (e) {
249
- const msg = e instanceof Error ? e.message : String(e);
250
- log.error(`Planner apply failed: ${msg}`);
251
- process.exitCode = 1;
252
- return;
253
- }
254
- }
255
- log.blank();
256
- log.info(bold("Next steps:"));
257
- if (wantsApply && options.applyPlannerDryRun !== true) {
258
- log.info(` 1) Open this repo in Cursor and run ${cyan("vibeops task start TASK-NNN")} for each backlog item.`);
259
- log.info(` 2) The planning prompt is still at ${cyan(relDisplay(cwd, promptAbs))} if you need to re-run the planner.`);
260
- log.info(` 3) To revise the brief only, edit ${cyan(relDisplay(cwd, briefAbs))} then:`);
261
- log.info(` ${dim("vibeops plan --from " + DEFAULT_BRIEF_REL + " [--apply-planner …]")}`);
262
- }
263
- else if (wantsApply && options.applyPlannerDryRun === true) {
264
- log.info(` 1) Re-run without ${cyan("--apply-dry-run")} to write files, commit, and (if enabled) Notion sync.`);
265
- log.info(` 2) Or paste ${cyan(relDisplay(cwd, promptAbs))} into Cursor for a manual planner pass.`);
266
- }
267
- else {
268
- log.info(` 1) Open a new Cursor chat and paste the full contents of ${cyan(relDisplay(cwd, promptAbs))}.`);
269
- log.info(` 2) Or run again with ${cyan("--apply-planner")}: the CLI calls the same LLM and writes ${cyan("docs/project/*")} + ${cyan("docs/tasks/*")} from the prompt (brief alone does not create TASK files).`);
270
- log.info(` 3) Regenerate only the prompt from an edited brief:`);
271
- log.info(` ${dim("vibeops plan --from " + DEFAULT_BRIEF_REL + " [--apply-planner …]")}`);
272
- }
273
- }
274
- async function resolvePlannerApplyExecution(inputs) {
275
- if (inputs.fromLlmProvider !== undefined) {
276
- return { provider: inputs.fromLlmProvider, model: inputs.fromLlmModel };
277
- }
278
- const tty = process.stdin.isTTY === true;
279
- if (inputs.cliProvider !== undefined) {
280
- const interactiveLogin = tty && !inputs.nonInteractive;
281
- const provider = await pickPlanLlmProvider(inputs.cwd, inputs.cliProvider, { interactiveLogin });
282
- if (provider === null)
283
- return null;
284
- let model = inputs.cliModel?.trim();
285
- if (!model && tty && !inputs.nonInteractive) {
286
- const picked = await pickPlanExecutionModel({
287
- provider,
288
- cwd: inputs.cwd,
289
- interactive: true,
290
- });
291
- model = picked?.trim();
292
- }
293
- return { provider, model: model && model.length > 0 ? model : undefined };
294
- }
295
- if (!tty || inputs.nonInteractive) {
296
- log.error("When using --apply-planner without an interactive LLM briefing in the same run, pass --provider openai | codex-oauth | cursor-agent (and optional --model / env defaults).");
297
- process.exitCode = 1;
298
- return null;
299
- }
300
- const provider = await pickPlanLlmProvider(inputs.cwd, undefined, { interactiveLogin: true });
301
- if (provider === null)
302
- return null;
303
- const model = await pickPlanExecutionModel({ provider, cwd: inputs.cwd, interactive: true });
304
- return { provider, model };
305
- }
306
- async function loadFromFile(inputs) {
307
- const { cwd, fromPath, nonInteractive, idea } = inputs;
308
- if (!(await pathExists(fromPath))) {
309
- log.error(`--from path does not exist: ${fromPath}`);
310
- process.exit(1);
311
- }
312
- const md = await readText(fromPath);
313
- log.step(`Loading brief: ${relDisplay(cwd, fromPath)}`);
314
- const parsed = parseBriefFromMarkdown(md);
315
- parsed.meta.source = "from-file";
316
- if (typeof idea === "string" && idea.length > 0) {
317
- const ideaParsed = parseIdea(idea);
318
- if (ideaParsed.projectName && parsed.brief.projectName.length === 0) {
319
- parsed.brief.projectName = ideaParsed.projectName;
320
- }
321
- if (ideaParsed.oneLineIdea && parsed.brief.oneLineIdea.length === 0) {
322
- parsed.brief.oneLineIdea = ideaParsed.oneLineIdea;
323
- }
324
- }
325
- const missing = findMissingRequired(parsed.brief);
326
- if (missing.length === 0) {
327
- return parsed;
328
- }
329
- if (nonInteractive) {
330
- log.warn(`Missing required fields: ${missing.join(", ")} → filling with placeholders and recording them in Assumptions.`);
331
- return parsed;
332
- }
333
- log.warn(`Missing required fields: ${missing.join(", ")} → asking only the missing questions.`);
334
- log.blank();
335
- const filled = await gatherBrief({
336
- cwd,
337
- idea,
338
- nonInteractive: false,
339
- seed: parsed.brief,
340
- });
341
- filled.meta.source = "from-file";
342
- return filled;
343
- }
@@ -1,155 +0,0 @@
1
- import { join, relative, resolve } from "node:path";
2
- import { pathExists, readText } from "../lib/filesystem.js";
3
- import { gitAllChangedFilesSinceTaskStart, gitCommitsAhead, gitLogOneline, readGitInfo, } from "../lib/git.js";
4
- import { bold, cyan, dim, green, log, red, yellow } from "../lib/logger.js";
5
- import { projectPaths } from "../lib/paths.js";
6
- import { findAcceptanceCriteria, findExpectedFiles, findTaskFile, hasNonEmptySection, readGitContext, readSection, readTaskFile, statusDisplay, } from "../lib/task.js";
7
- import { buildTaskPromptString } from "../lib/task-prompt.js";
8
- function relOrAbs(root, p) {
9
- const r = relative(root, p);
10
- return r === "" ? "." : r.startsWith("..") ? p : r;
11
- }
12
- function todayLogFileName() {
13
- const d = new Date();
14
- const y = d.getFullYear();
15
- const m = String(d.getMonth() + 1).padStart(2, "0");
16
- const day = String(d.getDate()).padStart(2, "0");
17
- return `${y}-${m}-${day}.md`;
18
- }
19
- export async function taskCheckCommand(taskId, options = {}) {
20
- const cwd = resolve(options.cwd ?? process.cwd());
21
- const paths = projectPaths(cwd);
22
- const agentName = options.agent ?? "reviewer";
23
- let missing = 0;
24
- const taskFile = await findTaskFile(paths.docsTasks, taskId);
25
- if (!taskFile) {
26
- log.error(`TASK not found: ${taskId} (looked in ${relOrAbs(cwd, paths.docsTasks)})`);
27
- process.exitCode = 1;
28
- return;
29
- }
30
- const meta = await readTaskFile(taskFile);
31
- const body = await readText(taskFile);
32
- log.info(bold(`vibeops task check ${meta.id}`));
33
- log.info(` ${dim("file")} ${relOrAbs(cwd, taskFile)}`);
34
- log.info(` ${dim("title")} ${meta.title || dim("(no title)")}`);
35
- log.info(` ${dim("status")} ${statusDisplay(meta.status)}`);
36
- log.blank();
37
- log.info(bold("Git"));
38
- const git = await readGitInfo(cwd);
39
- if (!git.isRepo) {
40
- log.info(` ${red("✗")} not a git repository`);
41
- missing++;
42
- }
43
- else {
44
- log.info(` ${dim("branch")} ${git.branch ?? dim("(detached)")}`);
45
- log.info(` ${dim("status")} ${git.dirty ? yellow("dirty") : green("clean")}`);
46
- }
47
- const ctx = await readGitContext(taskFile);
48
- if (ctx === null) {
49
- log.info(` ${yellow("·")} no Git Context section yet — run \`vibeops task start ${meta.id}\` first.`);
50
- missing++;
51
- }
52
- else {
53
- log.info(` ${dim("base")} ${ctx.baseBranch} @ ${ctx.baseCommit}`);
54
- log.info(` ${dim("task branch")} ${cyan(ctx.taskBranch)}`);
55
- log.info(` ${dim("started at")} ${ctx.startedAt}`);
56
- if (git.isRepo) {
57
- const range = `${ctx.baseCommit}..HEAD`;
58
- const summary = await gitAllChangedFilesSinceTaskStart(ctx.baseCommit, cwd);
59
- const ahead = await gitCommitsAhead(cwd, ctx.baseCommit);
60
- log.info(` ${dim("commits ahead")} ${ahead}`);
61
- log.info(` ${dim("working tree changed files")} ${summary.workingTree.length}`);
62
- log.info(` ${dim("committed changed files")} ${summary.committed.length}`);
63
- log.info(` ${dim("total changed files")} ${summary.all.length}`);
64
- const recent = await gitLogOneline(cwd, `${range}`);
65
- const head = recent.slice(0, 5);
66
- if (head.length > 0) {
67
- log.info(` ${dim("recent commits:")}`);
68
- for (const c of head)
69
- log.info(` · ${c.sha} ${c.message}`);
70
- }
71
- const expected = findExpectedFiles(body);
72
- if (expected.length > 0) {
73
- log.blank();
74
- log.info(bold("Expected Files to Change vs current diff"));
75
- const lower = new Set(summary.all.map((c) => c.toLowerCase()));
76
- let hit = 0;
77
- for (const e of expected) {
78
- const present = lower.has(e.toLowerCase());
79
- if (present)
80
- hit++;
81
- log.info(` ${present ? green("✓") : yellow("·")} ${e}${present ? "" : dim(" (not yet in diff)")}`);
82
- }
83
- log.info(dim(` match ${hit}/${expected.length} (${Math.round((hit / expected.length) * 100)}%)`));
84
- log.info(dim(` basis: working tree(${summary.workingTree.length}) ∪ committed(${summary.committed.length}) = total(${summary.all.length}) files`));
85
- }
86
- }
87
- }
88
- log.blank();
89
- log.info(bold("Acceptance Criteria"));
90
- const ac = findAcceptanceCriteria(body);
91
- if (ac.length === 0) {
92
- log.info(` ${dim("(none parsed — TASK file may not use \"1. …\" list format)")}`);
93
- }
94
- else {
95
- for (let i = 0; i < ac.length; i++) {
96
- log.info(` ${dim(`${i + 1}.`)} ${ac[i]}`);
97
- }
98
- log.info(dim(` → confirm each item manually with the diff before \`vibeops task done\`.`));
99
- }
100
- log.blank();
101
- log.info(bold("Docs touched in this round"));
102
- const docsChecks = [
103
- { label: "current-state (03)", path: join(paths.docsProject, "03-current-state.md"), required: false },
104
- { label: "current-state (05)", path: join(paths.docsProject, "05-current-state.md"), required: false },
105
- { label: `log (${todayLogFileName()})`, path: join(paths.docsLogs, todayLogFileName()), required: false },
106
- ];
107
- let anyDoc = false;
108
- for (const c of docsChecks) {
109
- const present = await pathExists(c.path);
110
- if (present)
111
- anyDoc = true;
112
- log.info(` ${present ? green("✓") : dim("·")} ${c.label} ${dim(relOrAbs(cwd, c.path))}`);
113
- }
114
- if (!anyDoc) {
115
- log.info(` ${yellow("!")} no current-state or today's log file found — Docs Agent must update them before \`task done\`.`);
116
- missing++;
117
- }
118
- log.blank();
119
- log.info(bold("TASK Result / Test Result"));
120
- const resultOk = hasNonEmptySection(body, "Result");
121
- const testResultOk = hasNonEmptySection(body, "Test Result");
122
- log.info(` ${resultOk ? green("✓") : yellow("·")} Result ${dim(resultOk ? readSection(body, "Result").split("\n")[0]?.slice(0, 80) ?? "" : "(empty / placeholder)")}`);
123
- log.info(` ${testResultOk ? green("✓") : yellow("·")} Test Result ${dim(testResultOk ? readSection(body, "Test Result").split("\n")[0]?.slice(0, 80) ?? "" : "(empty / placeholder)")}`);
124
- if (!resultOk)
125
- missing++;
126
- if (!testResultOk)
127
- missing++;
128
- log.blank();
129
- log.info(bold("Summary"));
130
- if (missing === 0) {
131
- log.ok("All checks present. Ready for `vibeops task done`.");
132
- }
133
- else {
134
- log.info(` ${yellow("!")} ${missing} item(s) need attention before \`vibeops task done\`.`);
135
- }
136
- log.blank();
137
- const promptResult = await buildTaskPromptString({
138
- projectRoot: paths.root,
139
- agentsDir: paths.vibeopsAgents,
140
- agentName,
141
- taskFilePath: taskFile,
142
- });
143
- if (promptResult.ok) {
144
- log.info(bold(`Cursor prompt (agent: ${agentName}):`));
145
- log.info(dim("─".repeat(60)));
146
- log.raw(promptResult.prompt.endsWith("\n") ? promptResult.prompt : `${promptResult.prompt}\n`);
147
- log.info(dim("─".repeat(60)));
148
- }
149
- else {
150
- log.info(dim(`(agent "${agentName}" not found — install \`.vibeops/agents/${agentName}.md\` or pass --agent <name>)`));
151
- }
152
- if (options.strict === true && missing > 0) {
153
- process.exitCode = 1;
154
- }
155
- }