@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
@@ -1,59 +1,11 @@
1
1
  import { join, relative } from "node:path";
2
2
  import { readTextOrNull } from "./filesystem.js";
3
3
  import { llmCompleteJson } from "./llm-complete.js";
4
- import { bold, dim, log } from "./logger.js";
5
- import { askInput, askSelect, yesNoSelect } from "./inquirer-helpers.js";
6
- import { pickPlanLlmProvider } from "./plan-llm-session.js";
7
- import { extractJsonObject } from "./plan-llm-openai.js";
8
4
  import { projectPaths } from "./paths.js";
9
5
  import { slugify } from "./slug.js";
10
- import { buildTaskBuildPrompt } from "./task-add-build-prompt.js";
6
+ import { buildMinimalTaskMarkdown, ensureV3Sections, normalizeTaskHeader, titleFromIdea, } from "./task-scaffold.js";
11
7
  import { statusDisplay } from "./task.js";
12
- const MAX_PLAN_TURNS = 36;
13
- const REQUIRED_SECTIONS = [
14
- "Status",
15
- "MVP Phase",
16
- "Goal",
17
- "Background",
18
- "Scope",
19
- "Out of Scope",
20
- "Acceptance Criteria",
21
- "Files to Inspect First",
22
- "Expected Files to Change",
23
- "Risks",
24
- "Test Plan",
25
- "Rollback Plan",
26
- "Implementation Plan",
27
- "Result",
28
- "Test Result",
29
- ];
30
- function parseTaskPlanTurn(jsonText) {
31
- const raw = JSON.parse(jsonText);
32
- const turn = raw.turn;
33
- if (turn === "done") {
34
- const taskMarkdown = typeof raw.taskMarkdown === "string" ? raw.taskMarkdown.trim() : "";
35
- const buildPromptMarkdown = typeof raw.buildPromptMarkdown === "string" ? raw.buildPromptMarkdown.trim() : "";
36
- if (taskMarkdown.length === 0) {
37
- throw new Error('Invalid JSON: "done" needs non-empty "taskMarkdown".');
38
- }
39
- if (buildPromptMarkdown.length === 0) {
40
- throw new Error('Invalid JSON: "done" needs non-empty "buildPromptMarkdown".');
41
- }
42
- return { turn: "done", taskMarkdown, buildPromptMarkdown };
43
- }
44
- if (turn === "question") {
45
- const message = typeof raw.message === "string" ? raw.message.trim() : "";
46
- if (message.length === 0)
47
- throw new Error('Invalid JSON: "question" needs "message".');
48
- const kindRaw = typeof raw.inputKind === "string" ? raw.inputKind : "text";
49
- const inputKind = kindRaw === "select" || kindRaw === "confirm" ? kindRaw : "text";
50
- const options = Array.isArray(raw.options)
51
- ? raw.options.filter((x) => typeof x === "string")
52
- : undefined;
53
- return { turn: "question", message, inputKind, options };
54
- }
55
- throw new Error('LLM JSON must use turn "question" or "done".');
56
- }
8
+ import { extractJsonObject } from "./plan-llm-openai.js";
57
9
  async function readSnippet(absPath, maxChars) {
58
10
  const text = await readTextOrNull(absPath);
59
11
  if (text === null)
@@ -61,208 +13,69 @@ async function readSnippet(absPath, maxChars) {
61
13
  const t = text.trim();
62
14
  return t.length <= maxChars ? t : `${t.slice(0, maxChars)}\n…`;
63
15
  }
64
- export async function loadTaskAddProjectContext(cwd) {
16
+ export async function loadProjectContextForLlm(cwd) {
65
17
  const paths = projectPaths(cwd);
66
- const chunks = [];
67
18
  const candidates = [
68
19
  join(paths.docsProject, "05-current-state.md"),
69
- join(paths.docsProject, "03-current-state.md"),
70
- join(paths.docsProject, "02-mvp-scope.md"),
71
20
  join(paths.docsProject, "03-architecture.md"),
72
- join(paths.docsProject, "04-tech-stack.md"),
73
21
  join(paths.docsProject, "06-decisions.md"),
74
22
  ];
23
+ const chunks = [];
75
24
  for (const p of candidates) {
76
- const snip = await readSnippet(p, 3500);
77
- if (snip) {
25
+ const snip = await readSnippet(p, 3000);
26
+ if (snip)
78
27
  chunks.push(`### ${relative(cwd, p)}\n\n${snip}`);
79
- }
80
28
  }
81
- return chunks.length > 0 ? chunks.join("\n\n") : "(no docs/project context files found)";
82
- }
83
- function quickTaskSystemPrompt() {
84
- return `You are a VibeOps TASK author. Respond with JSON only (no markdown fence).
85
-
86
- Schema:
87
- {
88
- "title": "short title without TASK- id prefix",
89
- "slug": "kebab-case-slug-max-60-chars-no-task-prefix",
90
- "markdown": "full TASK markdown file"
91
- }
92
-
93
- The markdown file must:
94
- - Start with # TASK-NNN: title (use the exact TASK id provided)
95
- - Include every section: ${REQUIRED_SECTIONS.join(", ")}
96
- - Status body: ${statusDisplay("planned")}
97
- - Result and Test Result: (not yet)
98
- - Keep Scope and Acceptance Criteria minimal but valid — the human will edit heavily in Cursor
99
- - Do not invent TASK numbers other than the one given`;
29
+ return chunks.length > 0 ? chunks.join("\n\n") : "(no project docs yet)";
100
30
  }
101
- function taskPlanSystemPrompt(taskId, projectName) {
102
- return `You are a VibeOps TASK planner. Interview the user, then produce a complete TASK and a Cursor build prompt.
31
+ function systemPrompt() {
32
+ return `You are a VibeOps TASK author. Reply with JSON only.
103
33
 
104
- Respond with JSON only (no markdown fence).
105
-
106
- Turn "question":
107
34
  {
108
- "turn": "question",
109
- "message": "…",
110
- "inputKind": "text" | "select" | "confirm",
111
- "options": ["…"] // required for select; optional for confirm (Yes/No implied)
35
+ "title": "short title without TASK- id",
36
+ "slug": "kebab-case-max-60-chars",
37
+ "markdown": "full TASK markdown"
112
38
  }
113
39
 
114
- Turn "done":
115
- {
116
- "turn": "done",
117
- "taskMarkdown": "full TASK file for ${taskId}",
118
- "buildPromptMarkdown": "markdown for Cursor — implement ${taskId} per the TASK"
40
+ markdown must include sections: Status, Goal, Scope, Out of Scope, Acceptance Criteria, Test Plan, Git Context, Result, Test Result.
41
+ Status body: ${statusDisplay("planned")}
42
+ Result and Test Result: (not yet)
43
+ Git Context: (populated by vibeops task add)
44
+ Use the exact TASK id given in the user message for the # heading.`;
119
45
  }
120
-
121
- taskMarkdown rules:
122
- - # ${taskId}: … as first line
123
- - All sections: ${REQUIRED_SECTIONS.join(", ")}
124
- - Status: ${statusDisplay("planned")}; Result / Test Result: (not yet)
125
- - Acceptance Criteria numbered and verifiable
126
- - Align with docs/project; put out-of-MVP items in Out of Scope
127
-
128
- buildPromptMarkdown rules:
129
- - Standalone implementer brief for Cursor (like mvp-build.md)
130
- - Reference the TASK path; list read-first docs; definition of done
131
- - Project: ${projectName}
132
-
133
- Ask enough questions to nail Scope, Acceptance Criteria, and Test Plan. Then finish with "done".`;
134
- }
135
- function parseQuickTaskJson(jsonText) {
136
- const raw = JSON.parse(jsonText);
137
- const title = typeof raw.title === "string" ? raw.title.trim() : "";
138
- const slug = typeof raw.slug === "string" ? slugify(raw.slug, "task") : "";
139
- const markdown = typeof raw.markdown === "string" ? raw.markdown.trim() : "";
140
- if (title.length === 0)
141
- throw new Error('LLM JSON missing "title".');
142
- if (markdown.length === 0)
143
- throw new Error('LLM JSON missing "markdown".');
144
- return { title, slug: slug.length > 0 ? slug : slugify(title), markdown };
145
- }
146
- export function normalizeTaskMarkdownHeader(markdown, taskId, title) {
147
- const lines = markdown.split("\n");
148
- const header = `# ${taskId}: ${title}`;
149
- if (lines.length > 0 && /^#\s+/.test(lines[0])) {
150
- lines[0] = header;
151
- return lines.join("\n");
152
- }
153
- return `${header}\n\n${markdown}`;
154
- }
155
- export function ensureTaskSections(markdown) {
156
- let body = markdown;
157
- for (const section of REQUIRED_SECTIONS) {
158
- const re = new RegExp(`^##\\s+${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "im");
159
- if (!re.test(body)) {
160
- body = `${body.trimEnd()}\n\n## ${section}\n\n(not yet)\n`;
161
- }
162
- }
163
- return body;
164
- }
165
- async function collectTaskPlanAnswer(q) {
166
- if (q.inputKind === "confirm") {
167
- const yes = await yesNoSelect({ message: q.message, defaultValue: true });
168
- return yes ? "Yes" : "No";
46
+ export async function llmScaffoldTask(params) {
47
+ try {
48
+ const context = await loadProjectContextForLlm(params.cwd);
49
+ const messages = [
50
+ { role: "system", content: systemPrompt() },
51
+ {
52
+ role: "user",
53
+ content: [`TASK id: ${params.taskId}`, `Idea: ${params.idea}`, "", context].join("\n"),
54
+ },
55
+ ];
56
+ const { text: raw, provider } = await llmCompleteJson(messages, { cwd: params.cwd });
57
+ const parsed = JSON.parse(extractJsonObject(raw));
58
+ const title = typeof parsed.title === "string"
59
+ ? parsed.title.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim()
60
+ : "";
61
+ const slug = typeof parsed.slug === "string" ? slugify(parsed.slug, "task") : slugify(title, "task");
62
+ const markdown = typeof parsed.markdown === "string" ? parsed.markdown.trim() : "";
63
+ if (title.length === 0 || markdown.length === 0)
64
+ return null;
65
+ const body = ensureV3Sections(normalizeTaskHeader(markdown, params.taskId, title));
66
+ return { title, slug, markdown: body, provider };
169
67
  }
170
- if (q.inputKind === "select" && q.options && q.options.length > 0) {
171
- return await askSelect({
172
- message: q.message,
173
- nonInteractive: false,
174
- choices: q.options,
175
- default: q.options[0],
176
- });
68
+ catch {
69
+ return null;
177
70
  }
178
- return await askInput({ message: q.message, nonInteractive: false, required: true });
179
71
  }
180
- export async function llmGenerateQuickTask(params) {
181
- const provider = await pickPlanLlmProvider(params.cwd, undefined, { interactiveLogin: false });
182
- if (provider === null)
183
- return null;
184
- const context = await loadTaskAddProjectContext(params.cwd);
185
- const latest = params.latestDone !== undefined && params.latestDone !== null
186
- ? `Latest done TASK: ${params.latestDone.id} — ${params.latestDone.title}`
187
- : "";
188
- const messages = [
189
- { role: "system", content: quickTaskSystemPrompt() },
190
- {
191
- role: "user",
192
- content: [
193
- `TASK id: ${params.taskId}`,
194
- `User intent: ${params.idea}`,
195
- latest,
196
- "",
197
- "Project context:",
198
- context,
199
- ]
200
- .filter((l) => l.length > 0)
201
- .join("\n"),
202
- },
203
- ];
204
- const { text: raw, provider: used } = await llmCompleteJson(messages, { cwd: params.cwd, provider });
205
- const parsed = parseQuickTaskJson(extractJsonObject(raw));
206
- const title = parsed.title.replace(/^TASK-\d+\s*[·:\-]\s*/i, "").trim() || parsed.title;
207
- const markdown = ensureTaskSections(normalizeTaskMarkdownHeader(parsed.markdown, params.taskId, title));
72
+ export function fallbackTaskDraft(taskId, idea) {
73
+ const title = titleFromIdea(idea);
74
+ const draft = { id: taskId, title, idea };
75
+ const slug = slugify(title);
208
76
  return {
209
77
  title,
210
- slug: parsed.slug,
211
- markdown,
212
- provider: used,
78
+ slug,
79
+ markdown: ensureV3Sections(normalizeTaskHeader(buildMinimalTaskMarkdown(draft), taskId, title)),
213
80
  };
214
81
  }
215
- export async function gatherTaskPlanViaLlm(params) {
216
- const provider = await pickPlanLlmProvider(params.cwd, undefined, { interactiveLogin: true });
217
- if (provider === null)
218
- return null;
219
- const context = await loadTaskAddProjectContext(params.cwd);
220
- const messages = [
221
- { role: "system", content: taskPlanSystemPrompt(params.taskId, params.projectName) },
222
- {
223
- role: "user",
224
- content: [
225
- `TASK id: ${params.taskId}`,
226
- params.seedIdea ? `Initial idea: ${params.seedIdea}` : "Ask what the user wants to build.",
227
- "",
228
- "Project context:",
229
- context,
230
- "",
231
- "Begin with your first question JSON.",
232
- ].join("\n"),
233
- },
234
- ];
235
- let lastQ = "";
236
- for (let step = 0; step < MAX_PLAN_TURNS; step++) {
237
- const { text: raw, provider: used } = await llmCompleteJson(messages, {
238
- cwd: params.cwd,
239
- provider,
240
- });
241
- const parsed = parseTaskPlanTurn(extractJsonObject(raw));
242
- if (parsed.turn === "done") {
243
- const titleMatch = /^#\s+TASK-\d+:\s*(.+)$/m.exec(parsed.taskMarkdown);
244
- const title = titleMatch?.[1]?.trim() ?? params.taskId;
245
- const markdown = ensureTaskSections(normalizeTaskMarkdownHeader(parsed.taskMarkdown, params.taskId, title));
246
- return {
247
- markdown,
248
- buildPromptMarkdown: parsed.buildPromptMarkdown,
249
- provider: used,
250
- };
251
- }
252
- log.blank();
253
- log.info(dim(`Planning ${params.taskId} — question ${step + 1}`));
254
- log.info(bold(parsed.message));
255
- log.blank();
256
- const answer = await collectTaskPlanAnswer(parsed);
257
- lastQ = parsed.message;
258
- messages.push({ role: "assistant", content: raw });
259
- messages.push({
260
- role: "user",
261
- content: `Previous Q: ${lastQ}\nUser answer: ${answer}\n\nContinue with next JSON (question or done).`,
262
- });
263
- }
264
- throw new Error(`Task planning stopped after ${MAX_PLAN_TURNS} turns — try a shorter scope or run again.`);
265
- }
266
- export function defaultBuildPromptForTask(projectName, taskId, taskRelativePath) {
267
- return buildTaskBuildPrompt({ projectName, taskId, taskRelativePath });
268
- }
@@ -0,0 +1,68 @@
1
+ import { relative } from "node:path";
2
+ import { gitBranchExists, readGitInfo } from "./git.js";
3
+ import { readGitContext } from "./task.js";
4
+ import { isOnTaskBranch, loadActionableTasks, pickActiveTask, pickInProgressTask, } from "./task.js";
5
+ /** TASK that blocks `task add` — in progress or open on current task branch. */
6
+ export async function findBlockingTask(paths, cwd) {
7
+ const tasks = await loadActionableTasks(paths.docsTasks);
8
+ const inProgress = pickInProgressTask(tasks);
9
+ if (inProgress !== null)
10
+ return inProgress;
11
+ const git = await readGitInfo(cwd);
12
+ if (git.isRepo && typeof git.branch === "string" && git.branch.startsWith("task/")) {
13
+ for (const t of tasks) {
14
+ if (t.status === "done")
15
+ continue;
16
+ const ctx = await readGitContext(t.filePath);
17
+ if (isOnTaskBranch(git.branch, ctx))
18
+ return t;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ export async function pickFocusTask(paths, cwd) {
24
+ return pickActiveTask(paths.docsTasks, cwd);
25
+ }
26
+ export function relPath(cwd, filePath) {
27
+ const r = relative(cwd, filePath).replace(/\\/g, "/");
28
+ return r.startsWith("..") ? filePath : r;
29
+ }
30
+ export function computeNextHint(input) {
31
+ if (!input.isVibeopsProject)
32
+ return "init";
33
+ if (input.focus === null)
34
+ return "task-add";
35
+ if (input.focus.status === "done")
36
+ return "task-add";
37
+ if (!input.resultFilled || !input.testFilled) {
38
+ return input.onTaskBranch === true ? "cursor-implement" : "cursor-plan";
39
+ }
40
+ return "task-done";
41
+ }
42
+ export function hintToText(hint, cwd, task) {
43
+ switch (hint) {
44
+ case "init":
45
+ return "Run `vibeops init` in this directory.";
46
+ case "task-add":
47
+ return "Run `vibeops task add` to start the next slice.";
48
+ case "task-done":
49
+ return task ? `Run \`vibeops task done ${task.id}\`.` : "Run `vibeops task done`.";
50
+ case "cursor-plan":
51
+ return task
52
+ ? `In Cursor Ask: @${relPath(cwd, task.filePath)} — refine Scope / AC.`
53
+ : "Open the TASK file in Cursor Ask.";
54
+ case "cursor-implement":
55
+ return task
56
+ ? `In Cursor Agent: @${relPath(cwd, task.filePath)} — implement, then task done.`
57
+ : "Implement per the TASK file in Cursor.";
58
+ }
59
+ }
60
+ export async function listInProgressTasks(paths) {
61
+ return (await loadActionableTasks(paths.docsTasks)).filter((t) => t.status === "in_progress");
62
+ }
63
+ export async function taskBranchExistsFor(cwd, task) {
64
+ const ctx = await readGitContext(task.filePath);
65
+ if (ctx === null)
66
+ return false;
67
+ return gitBranchExists(cwd, ctx.taskBranch);
68
+ }
@@ -0,0 +1,148 @@
1
+ import { join } from "node:path";
2
+ import { readText, readTextOrNull, writeText } from "./filesystem.js";
3
+ import { llmCompleteJson } from "./llm-complete.js";
4
+ import { gitDiffNameOnly, readGitInfo } from "./git.js";
5
+ import { projectPaths, PROJECT_MEMORY_FILES } from "./paths.js";
6
+ import { extractJsonObject } from "./plan-llm-openai.js";
7
+ import { readSection, updateTaskSection } from "./task.js";
8
+ function localDateFilename() {
9
+ const d = new Date();
10
+ const y = d.getFullYear();
11
+ const m = String(d.getMonth() + 1).padStart(2, "0");
12
+ const day = String(d.getDate()).padStart(2, "0");
13
+ return `${y}-${m}-${day}.md`;
14
+ }
15
+ async function readOptional(path) {
16
+ return readTextOrNull(path);
17
+ }
18
+ export async function llmCompleteTaskDone(params) {
19
+ const git = await readGitInfo(params.cwd);
20
+ let diffSummary = "";
21
+ if (git.isRepo) {
22
+ const names = await gitDiffNameOnly(params.cwd);
23
+ diffSummary = names.slice(0, 40).join("\n") || "(no diff vs HEAD index yet)";
24
+ }
25
+ const paths = projectPaths(params.cwd);
26
+ const currentState = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.currentState))) ?? "";
27
+ const architecture = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.architecture))) ?? "";
28
+ const decisions = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.decisions))) ?? "";
29
+ const messages = [
30
+ {
31
+ role: "system",
32
+ content: `You close a VibeOps TASK. Reply with JSON only:
33
+ {
34
+ "result": "markdown for TASK Result section — facts, paths, commands",
35
+ "testResult": "markdown for Test Result — commands and outcomes",
36
+ "currentStateMarkdown": "full replacement for docs/project/05-current-state.md OR null to skip",
37
+ "decisionsAppend": "markdown bullets to append under ## Decisions in 06-decisions.md OR null",
38
+ "architectureUpdate": "full replacement for 03-architecture.md OR null if structure unchanged",
39
+ "logLine": "one line for docs/logs/YYYY-MM-DD.md"
40
+ }
41
+ Be factual. Do not invent files not in the diff.`,
42
+ },
43
+ {
44
+ role: "user",
45
+ content: [
46
+ `TASK: ${params.taskId} — ${params.taskTitle}`,
47
+ `File: ${params.taskFileRel}`,
48
+ "",
49
+ "TASK body:",
50
+ params.taskBody.slice(0, 8000),
51
+ "",
52
+ "Changed paths (git):",
53
+ diffSummary,
54
+ "",
55
+ "Current 05-current-state.md:",
56
+ currentState.slice(0, 4000),
57
+ "",
58
+ "Current 03-architecture.md:",
59
+ architecture.slice(0, 4000),
60
+ "",
61
+ "Current 06-decisions.md:",
62
+ decisions.slice(0, 2000),
63
+ ].join("\n"),
64
+ },
65
+ ];
66
+ try {
67
+ const { text: raw, provider } = await llmCompleteJson(messages, { cwd: params.cwd });
68
+ const parsed = JSON.parse(extractJsonObject(raw));
69
+ const result = typeof parsed.result === "string" ? parsed.result.trim() : "";
70
+ const testResult = typeof parsed.testResult === "string" ? parsed.testResult.trim() : "";
71
+ if (result.length === 0 || testResult.length === 0)
72
+ return null;
73
+ return {
74
+ result,
75
+ testResult,
76
+ currentStateMarkdown: typeof parsed.currentStateMarkdown === "string" && parsed.currentStateMarkdown.trim().length > 0
77
+ ? parsed.currentStateMarkdown.trim()
78
+ : null,
79
+ decisionsAppend: typeof parsed.decisionsAppend === "string" && parsed.decisionsAppend.trim().length > 0
80
+ ? parsed.decisionsAppend.trim()
81
+ : null,
82
+ architectureUpdate: typeof parsed.architectureUpdate === "string" && parsed.architectureUpdate.trim().length > 0
83
+ ? parsed.architectureUpdate.trim()
84
+ : null,
85
+ logLine: typeof parsed.logLine === "string" && parsed.logLine.trim().length > 0
86
+ ? parsed.logLine.trim()
87
+ : `${params.taskId} done.`,
88
+ provider,
89
+ };
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ export async function applyTaskDoneMemory(cwd, patch) {
96
+ const paths = projectPaths(cwd);
97
+ const updated = [];
98
+ const skipped = [];
99
+ if (patch.currentStateMarkdown) {
100
+ const p = join(paths.root, PROJECT_MEMORY_FILES.currentState);
101
+ await writeText(p, `${patch.currentStateMarkdown.trimEnd()}\n`);
102
+ updated.push(PROJECT_MEMORY_FILES.currentState);
103
+ }
104
+ else {
105
+ skipped.push(PROJECT_MEMORY_FILES.currentState);
106
+ }
107
+ if (patch.architectureUpdate) {
108
+ const p = join(paths.root, PROJECT_MEMORY_FILES.architecture);
109
+ await writeText(p, `${patch.architectureUpdate.trimEnd()}\n`);
110
+ updated.push(PROJECT_MEMORY_FILES.architecture);
111
+ }
112
+ else {
113
+ skipped.push(PROJECT_MEMORY_FILES.architecture);
114
+ }
115
+ if (patch.decisionsAppend) {
116
+ const p = join(paths.root, PROJECT_MEMORY_FILES.decisions);
117
+ const existing = (await readOptional(p)) ?? "# Decisions\n\n";
118
+ const next = `${existing.trimEnd()}\n\n${patch.decisionsAppend.trim()}\n`;
119
+ await writeText(p, next);
120
+ updated.push(PROJECT_MEMORY_FILES.decisions);
121
+ }
122
+ else {
123
+ skipped.push(PROJECT_MEMORY_FILES.decisions);
124
+ }
125
+ const logPath = join(paths.docsLogs, localDateFilename());
126
+ const logExisting = (await readOptional(logPath)) ?? `# ${localDateFilename().replace(".md", "")}\n\n`;
127
+ await writeText(logPath, `${logExisting.trimEnd()}\n\n- ${patch.logLine}\n`);
128
+ updated.push(relativeLogPath(logPath, paths.root));
129
+ return { updated, skipped };
130
+ }
131
+ function relativeLogPath(abs, root) {
132
+ return abs.startsWith(root) ? abs.slice(root.length + 1) : abs;
133
+ }
134
+ export async function writeTaskResultSections(taskFile, result, testResult) {
135
+ await updateTaskSection(taskFile, "Result", result);
136
+ await updateTaskSection(taskFile, "Test Result", testResult);
137
+ }
138
+ /** Minimal fallback when LLM unavailable — git-only sections from diff. */
139
+ export async function fallbackResultSections(cwd, taskId, taskFile) {
140
+ const names = await gitDiffNameOnly(cwd);
141
+ const body = await readText(taskFile);
142
+ const goal = readSection(body, "Goal").slice(0, 200);
143
+ const result = [`Completed ${taskId}.`, goal.length > 0 ? `Goal: ${goal}` : "", names.length > 0 ? `Paths: ${names.slice(0, 15).join(", ")}` : ""]
144
+ .filter((l) => l.length > 0)
145
+ .join("\n");
146
+ const testResult = names.length > 0 ? `Changed: ${names.join(", ")}` : "Manual verification required.";
147
+ return { result, testResult };
148
+ }