@goodtek/vibeops 0.7.22 → 1.1.1

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 (144) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +51 -380
  3. package/dist/bootstrap/installer.js +42 -20
  4. package/dist/bootstrap/manifest.js +23 -3
  5. package/dist/cli.js +69 -154
  6. package/dist/commands/init.js +279 -114
  7. package/dist/commands/llm.js +177 -0
  8. package/dist/commands/status.js +181 -8
  9. package/dist/commands/task-add.js +89 -162
  10. package/dist/commands/task-done.js +112 -73
  11. package/dist/lib/config.js +77 -113
  12. package/dist/lib/env.js +32 -0
  13. package/dist/lib/git-config.js +21 -0
  14. package/dist/lib/git-host.js +12 -0
  15. package/dist/lib/git-policy.js +67 -0
  16. package/dist/lib/git-remote.js +108 -0
  17. package/dist/lib/git.js +44 -1
  18. package/dist/lib/init-clients.js +55 -0
  19. package/dist/lib/llm-complete.js +18 -19
  20. package/dist/lib/llm-connect.js +150 -0
  21. package/dist/lib/llm-preference.js +102 -0
  22. package/dist/lib/llm-status.js +19 -0
  23. package/dist/lib/paths.js +7 -3
  24. package/dist/lib/plan-llm-openai.js +0 -63
  25. package/dist/lib/pr-create.js +66 -0
  26. package/dist/lib/resolve-task.js +34 -0
  27. package/dist/lib/task-add-llm.js +53 -233
  28. package/dist/lib/task-context.js +68 -0
  29. package/dist/lib/task-done-llm.js +155 -0
  30. package/dist/lib/task-pr-llm.js +50 -0
  31. package/dist/lib/task-push-pr.js +131 -0
  32. package/dist/lib/task-scaffold.js +39 -43
  33. package/dist/lib/task-start.js +100 -0
  34. package/dist/lib/task-summary.js +12 -239
  35. package/dist/lib/task.js +30 -24
  36. package/dist/types/config.js +0 -16
  37. package/package.json +14 -14
  38. package/templates/.cursor/rules/02-git-safety.mdc +12 -0
  39. package/templates/AGENTS.md +27 -87
  40. package/templates/clients/claude/CLAUDE.md +28 -0
  41. package/templates/clients/cursor/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  42. package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
  43. package/templates/clients/cursor/.cursor/rules/03-docs-on-done.mdc +13 -0
  44. package/templates/core/AGENTS.md +44 -0
  45. package/templates/core/docs/logs/README.md +7 -0
  46. package/templates/core/docs/project/03-architecture.md +15 -0
  47. package/templates/core/docs/project/05-current-state.md +15 -0
  48. package/templates/core/docs/project/06-decisions.md +9 -0
  49. package/templates/core/docs/tasks/TASK-000-template.md +37 -0
  50. package/templates/skills-shared/implement-task/SKILL.md +22 -0
  51. package/templates/skills-shared/plan-task/SKILL.md +19 -0
  52. package/dist/agent/loader.js +0 -71
  53. package/dist/agent/prompt.js +0 -66
  54. package/dist/commands/agent-list.js +0 -32
  55. package/dist/commands/agent-prompt.js +0 -59
  56. package/dist/commands/agent-show.js +0 -26
  57. package/dist/commands/done.js +0 -160
  58. package/dist/commands/github-init.js +0 -554
  59. package/dist/commands/github-status.js +0 -164
  60. package/dist/commands/next.js +0 -283
  61. package/dist/commands/notion-init.js +0 -764
  62. package/dist/commands/notion-sync.js +0 -405
  63. package/dist/commands/notion-test.js +0 -595
  64. package/dist/commands/plan.js +0 -146
  65. package/dist/commands/rollback.js +0 -126
  66. package/dist/commands/start.js +0 -129
  67. package/dist/commands/task-check.js +0 -155
  68. package/dist/commands/task-generate.js +0 -206
  69. package/dist/commands/task-pull.js +0 -277
  70. package/dist/commands/task-rollback.js +0 -174
  71. package/dist/commands/task-start.js +0 -102
  72. package/dist/commands/task-status.js +0 -122
  73. package/dist/lib/brief.js +0 -356
  74. package/dist/lib/cursor-task-prompt.js +0 -60
  75. package/dist/lib/done-follow-up.js +0 -308
  76. package/dist/lib/github-cli.js +0 -247
  77. package/dist/lib/guide-state.js +0 -69
  78. package/dist/lib/mvp-artifacts.js +0 -204
  79. package/dist/lib/mvp-constants.js +0 -15
  80. package/dist/lib/mvp-diff-summary.js +0 -43
  81. package/dist/lib/mvp-done-summary.js +0 -335
  82. package/dist/lib/notion-client.js +0 -459
  83. package/dist/lib/notion-discovery.js +0 -671
  84. package/dist/lib/notion-env.js +0 -140
  85. package/dist/lib/notion-mappers.js +0 -148
  86. package/dist/lib/notion-schema.js +0 -272
  87. package/dist/lib/notion-sync.js +0 -337
  88. package/dist/lib/notion-target.js +0 -247
  89. package/dist/lib/package-json.js +0 -133
  90. package/dist/lib/plan-apply-planner.js +0 -131
  91. package/dist/lib/plan-codex-pkce.js +0 -234
  92. package/dist/lib/plan-llm-models.js +0 -237
  93. package/dist/lib/plan-llm-normalize.js +0 -146
  94. package/dist/lib/plan-llm-session.js +0 -682
  95. package/dist/lib/plan-post-apply-setup.js +0 -116
  96. package/dist/lib/project-docs.js +0 -95
  97. package/dist/lib/prompt-builder.js +0 -135
  98. package/dist/lib/resolve-command-task.js +0 -40
  99. package/dist/lib/task-add-build-prompt.js +0 -39
  100. package/dist/lib/task-add-cursor.js +0 -81
  101. package/dist/lib/task-generator.js +0 -183
  102. package/dist/lib/task-merge.js +0 -220
  103. package/dist/lib/task-prompt.js +0 -23
  104. package/dist/lib/task-pull.js +0 -354
  105. package/dist/lib/task-status-brief.js +0 -132
  106. package/dist/lib/workflow-guide.js +0 -485
  107. package/dist/status/collector.js +0 -103
  108. package/dist/status/format.js +0 -181
  109. package/dist/types/brief.js +0 -126
  110. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  111. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  112. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  113. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  114. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  115. package/templates/.vibeops/agents/architect.md +0 -47
  116. package/templates/.vibeops/agents/builder.md +0 -38
  117. package/templates/.vibeops/agents/docs.md +0 -54
  118. package/templates/.vibeops/agents/orchestrator.md +0 -40
  119. package/templates/.vibeops/agents/planner.md +0 -60
  120. package/templates/.vibeops/agents/recovery.md +0 -49
  121. package/templates/.vibeops/agents/reviewer.md +0 -47
  122. package/templates/.vibeops/agents/tester.md +0 -43
  123. package/templates/.vibeops/prompts/create-plan.md +0 -33
  124. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  125. package/templates/.vibeops/prompts/implement-task.md +0 -39
  126. package/templates/.vibeops/prompts/review-task.md +0 -34
  127. package/templates/.vibeops/prompts/rollback.md +0 -32
  128. package/templates/.vibeops/prompts/start-project.md +0 -39
  129. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  130. package/templates/.vibeops/workflows/project-start.md +0 -73
  131. package/templates/.vibeops/workflows/rollback.md +0 -45
  132. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  133. package/templates/docs/logs/README.md +0 -38
  134. package/templates/docs/project/00-overview.md +0 -27
  135. package/templates/docs/project/01-requirements.md +0 -30
  136. package/templates/docs/project/02-mvp-scope.md +0 -36
  137. package/templates/docs/project/03-architecture.md +0 -34
  138. package/templates/docs/project/04-tech-stack.md +0 -29
  139. package/templates/docs/project/05-current-state.md +0 -35
  140. package/templates/docs/project/06-decisions.md +0 -20
  141. package/templates/docs/project/07-backlog.md +0 -23
  142. package/templates/docs/project/08-env.md +0 -29
  143. package/templates/docs/project/09-deployment.md +0 -28
  144. package/templates/docs/tasks/TASK-000-template.md +0 -72
@@ -1,204 +0,0 @@
1
- import { readdir } from "node:fs/promises";
2
- import { basename, join } from "node:path";
3
- import { isDirectory, pathExists, writeText } from "./filesystem.js";
4
- import { MVP_BUILD_PROMPT_REL, TASK_MVP_ID } from "./mvp-constants.js";
5
- import { excerptDoneSummary, readLastDoneSummary, renderPriorIterationSection, } from "./mvp-done-summary.js";
6
- import { projectPaths } from "./paths.js";
7
- import { slugify } from "./slug.js";
8
- function bulletList(items, empty = "(none listed in brief)") {
9
- if (items.length === 0)
10
- return `- ${empty}`;
11
- return items.map((x) => `- ${x}`).join("\n");
12
- }
13
- export function renderMvpTaskMarkdown(bundle, priorSummary) {
14
- const { brief } = bundle;
15
- const title = `MVP — ${brief.projectName}`;
16
- const priorBlock = typeof priorSummary === "string" && priorSummary.trim().length > 0
17
- ? `${renderPriorIterationSection(excerptDoneSummary(priorSummary, 55))}\n`
18
- : "";
19
- const criteria = brief.successCriteria.trim().length > 0
20
- ? `1. ${brief.successCriteria.trim()}`
21
- : "1. The MVP delivers the scoped features below and matches the success criteria in `docs/project/02-mvp-scope.md` after `vibeops plan`.";
22
- const featureCriteria = brief.mvpFeatures
23
- .slice(0, 8)
24
- .map((f, i) => `${i + 2}. ${f} is implemented and verifiable.`)
25
- .join("\n");
26
- return `# ${TASK_MVP_ID} · ${title}
27
-
28
- > Single MVP TASK for the v2 workflow. Implementation happens in **Cursor** using \`${MVP_BUILD_PROMPT_REL}\` (drag into chat). Git rails: \`vibeops start\` → build → \`vibeops done\`.
29
-
30
- ## Status
31
-
32
- planned
33
-
34
- ## MVP Phase
35
-
36
- MVP (full scope)
37
-
38
- ## Goal
39
-
40
- Ship the **entire MVP** for **${brief.projectName}**: ${brief.oneLineIdea}
41
-
42
- ## Background
43
-
44
- Generated from \`vibeops plan\` (${bundle.meta.source}). Tech: ${brief.frontend} / ${brief.backend} / ${brief.database} (${brief.dbLayer}), package manager ${brief.packageManager}.
45
-
46
- ${priorBlock}## Scope
47
-
48
- ${bulletList(brief.mvpFeatures, "(add MVP features in the brief)")}
49
-
50
- ## Out of Scope
51
-
52
- ${bulletList(brief.outOfScope)}
53
-
54
- ## Acceptance Criteria
55
-
56
- ${criteria}
57
- ${featureCriteria.length > 0 ? featureCriteria : ""}
58
-
59
- ## Files to Inspect First
60
-
61
- - \`AGENTS.md\`
62
- - \`docs/project/05-current-state.md\` (or \`03-current-state.md\` if present)
63
- - \`docs/project/02-mvp-scope.md\`
64
- - \`docs/project/03-architecture.md\`
65
- - \`${MVP_BUILD_PROMPT_REL}\`
66
-
67
- ## Expected Files to Change
68
-
69
- - (application code and config as required by the MVP — list concrete paths as you implement)
70
-
71
- ## Risks
72
-
73
- ${bulletList(brief.risks)}
74
-
75
- ## Test Plan
76
-
77
- - Run the project's test/lint scripts if present (\`pnpm test\`, \`pnpm lint\`, etc.).
78
- - Manually verify each Acceptance Criterion in Cursor or the running app.
79
- - Record commands and outcomes in **Test Result** before \`vibeops done\`.
80
-
81
- ## Rollback Plan
82
-
83
- - \`vibeops task rollback ${TASK_MVP_ID}\` for guidance; destructive steps require \`--confirm\`.
84
- - Or delete branch \`task/mvp-*\` after switching to the base branch.
85
-
86
- ## Git Context
87
-
88
- (populated by \`vibeops start\` or \`vibeops task start ${TASK_MVP_ID}\`)
89
-
90
- ## Notion Page
91
-
92
- (optional — \`vibeops notion sync\` when Notion is enabled)
93
-
94
- ## Implementation Plan
95
-
96
- 1. \`vibeops start\` (creates \`task/mvp-*\` branch).
97
- 2. Drag \`${MVP_BUILD_PROMPT_REL}\` into a **new Cursor chat** and implement until Acceptance Criteria pass.
98
- 3. Fill **Result** and **Test Result**, then \`vibeops done\`.
99
-
100
- ## Result
101
-
102
- (not yet)
103
-
104
- ## Test Result
105
-
106
- (not yet)
107
-
108
- ## Review Notes
109
-
110
- (not yet)
111
- `;
112
- }
113
- export function buildMvpBuildPrompt(bundle, taskRelativePath, priorSummary) {
114
- const { brief } = bundle;
115
- const priorSection = typeof priorSummary === "string" && priorSummary.trim().length > 0
116
- ? `\n## Previous iteration (read first)\n\n${excerptDoneSummary(priorSummary, 80)}\n\n---\n`
117
- : "";
118
- return `# MVP build — ${brief.projectName}
119
- ${priorSection}
120
-
121
- You are the **lead implementer** in Cursor for this repository. Deliver the **full MVP** in one focused effort. VibeOps owns Git/docs rails; **you** own code and verification.
122
-
123
- ## Source of truth (read first)
124
-
125
- 1. \`AGENTS.md\` and \`.cursor/rules/\`
126
- 2. \`docs/project/05-current-state.md\` (or \`03-current-state.md\`)
127
- 3. \`docs/project/02-mvp-scope.md\`, \`03-architecture.md\`, \`04-tech-stack.md\`
128
- 4. TASK file: \`${taskRelativePath}\` — **only** work within its Scope / Acceptance Criteria
129
-
130
- ## Project brief
131
-
132
- - **Idea:** ${brief.oneLineIdea}
133
- - **Type:** ${brief.projectType}
134
- - **Users:** ${brief.targetUsers.join(", ") || "(see brief)"}
135
- - **Problem:** ${brief.coreProblem}
136
- - **Stack:** ${brief.frontend} · ${brief.backend} · ${brief.database} · ${brief.dbLayer} · ${brief.packageManager}
137
- - **Deploy:** ${brief.deploymentTargets.join(", ") || "TBD"}
138
- - **Auth:** ${brief.authRequirements.join(", ") || "none"}
139
- - **Integrations (MVP):** ${brief.integrations.join(", ") || "none"}
140
- - **Success:** ${brief.successCriteria}
141
-
142
- ### MVP features (in scope)
143
-
144
- ${bulletList(brief.mvpFeatures)}
145
-
146
- ### Explicitly out of scope
147
-
148
- ${bulletList(brief.outOfScope)}
149
-
150
- ## How to work (orchestration — in this chat, not via CLI)
151
-
152
- - Implement **one TASK only**: \`${TASK_MVP_ID}\`. Do not start other TASK ids unless the human asks.
153
- - **Search** the repo before adding files; reuse existing patterns.
154
- - You may delegate focused sub-steps with Cursor's **Task** tool (explore, shell) — max **3** attempts per sub-problem, then stop and report blockers.
155
- - Do **not** rely on \`vibeops agent prompt\` paste loops or multi-TASK \`task generate\` for this MVP pass.
156
- - No features outside \`docs/project/02-mvp-scope.md\` unless the human expands scope in Git docs first.
157
-
158
- ## Definition of done
159
-
160
- 1. Every item in **Acceptance Criteria** in \`${taskRelativePath}\` is met.
161
- 2. **Result** and **Test Result** sections in that TASK file are filled with facts (paths, commands, outcomes).
162
- 3. Working tree is ready for \`vibeops done\` (commit on the task branch if needed).
163
-
164
- ## Risks to watch
165
-
166
- ${bulletList(brief.risks)}
167
-
168
- ---
169
-
170
- *Generated by \`vibeops plan\`. Re-run plan to refresh after brief changes.*
171
- `;
172
- }
173
- export async function findMvpTaskFile(tasksDir) {
174
- if (!(await isDirectory(tasksDir)))
175
- return null;
176
- const entries = await readdir(tasksDir, { withFileTypes: true });
177
- const match = entries
178
- .filter((e) => e.isFile() && /^TASK-mvp/i.test(e.name) && e.name.endsWith(".md"))
179
- .map((e) => join(tasksDir, e.name))
180
- .sort();
181
- return match[0] ?? null;
182
- }
183
- export async function writeMvpArtifacts(cwd, bundle) {
184
- const paths = projectPaths(cwd);
185
- const priorSummary = await readLastDoneSummary(cwd);
186
- const slug = slugify(bundle.brief.projectName, "mvp");
187
- const fileName = `TASK-mvp-${slug}.md`;
188
- const taskFile = join(paths.docsTasks, fileName);
189
- const existing = await findMvpTaskFile(paths.docsTasks);
190
- const targetTask = existing !== null && basename(existing) !== fileName ? existing : taskFile;
191
- await writeText(targetTask, renderMvpTaskMarkdown(bundle, priorSummary));
192
- const buildPromptFile = join(cwd, MVP_BUILD_PROMPT_REL);
193
- const taskRelative = targetTask.startsWith(cwd) ? targetTask.slice(cwd.length + 1) : targetTask;
194
- await writeText(buildPromptFile, buildMvpBuildPrompt(bundle, taskRelative, priorSummary));
195
- return {
196
- taskFile: targetTask,
197
- taskRelative,
198
- buildPromptFile,
199
- buildPromptRelative: MVP_BUILD_PROMPT_REL,
200
- };
201
- }
202
- export async function mvpBuildPromptExists(cwd) {
203
- return pathExists(join(cwd, MVP_BUILD_PROMPT_REL));
204
- }
@@ -1,15 +0,0 @@
1
- /** Fixed id for the single MVP build TASK (v2 workflow). */
2
- export const TASK_MVP_ID = "TASK-mvp";
3
- export const MVP_BUILD_PROMPT_REL = ".vibeops/generated/mvp-build.md";
4
- export const NEXT_TASK_SUGGESTION_REL = ".vibeops/generated/next-task-suggestion.md";
5
- /** CLI aliases → canonical TASK id */
6
- export function resolveTaskRef(ref) {
7
- if (ref === undefined || ref.trim().length === 0)
8
- return TASK_MVP_ID;
9
- const t = ref.trim();
10
- if (/^mvp$/i.test(t))
11
- return TASK_MVP_ID;
12
- if (/^task-mvp$/i.test(t))
13
- return TASK_MVP_ID;
14
- return t.toUpperCase().startsWith("TASK-") ? t : `TASK-${t}`;
15
- }
@@ -1,43 +0,0 @@
1
- import { readGitContext } from "./task.js";
2
- import { runGit } from "./git.js";
3
- export async function summarizeTaskDiff(cwd, taskFile) {
4
- const ctx = await readGitContext(taskFile);
5
- if (ctx === null || ctx.baseCommit.length === 0)
6
- return null;
7
- const range = `${ctx.baseCommit}..HEAD`;
8
- const lines = [
9
- `Git range: \`${range}\` (base \`${ctx.baseBranch}\` → branch \`${ctx.taskBranch}\`)`,
10
- "",
11
- ];
12
- try {
13
- const log = await runGit(cwd, ["log", "--oneline", range]);
14
- const commits = log.stdout.trim().split("\n").filter((l) => l.length > 0);
15
- lines.push("### Commits");
16
- if (commits.length === 0) {
17
- lines.push("(no commits on task branch since start)");
18
- }
19
- else {
20
- for (const c of commits.slice(0, 30))
21
- lines.push(`- ${c}`);
22
- if (commits.length > 30)
23
- lines.push(`- … and ${commits.length - 30} more`);
24
- }
25
- lines.push("");
26
- }
27
- catch {
28
- lines.push("### Commits");
29
- lines.push("(could not read git log)");
30
- lines.push("");
31
- }
32
- try {
33
- const stat = await runGit(cwd, ["diff", "--stat", range]);
34
- lines.push("### Diff stat");
35
- const body = stat.stdout.trim();
36
- lines.push(body.length > 0 ? "```\n" + body + "\n```" : "(no file changes vs base commit)");
37
- }
38
- catch {
39
- lines.push("### Diff stat");
40
- lines.push("(could not read git diff)");
41
- }
42
- return lines.join("\n");
43
- }
@@ -1,335 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { join, relative } from "node:path";
3
- import { isDirectory, pathExists, readText, readTextOrNull, writeText } from "./filesystem.js";
4
- import { runGit } from "./git.js";
5
- import { llmCompleteJson, llmCompleteText, resolveAvailableLlmProvider } from "./llm-complete.js";
6
- import { cyan, dim, log } from "./logger.js";
7
- import { summarizeTaskDiff } from "./mvp-diff-summary.js";
8
- import { findAcceptanceCriteria, hasNonEmptySection, readGitContext, readSection, updateTaskSection, } from "./task.js";
9
- export const LAST_DONE_SUMMARY_REL = ".vibeops/generated/last-done-summary.md";
10
- export const DONE_SUMMARIES_DIR = ".vibeops/generated/done-summaries";
11
- const MAX_PATCH_CHARS = 48_000;
12
- function rel(cwd, abs) {
13
- const r = relative(cwd, abs);
14
- return r.length === 0 ? "." : r.startsWith("..") ? abs : r;
15
- }
16
- async function gitNameStatus(cwd, range) {
17
- try {
18
- const res = await runGit(cwd, ["diff", "--name-status", range]);
19
- const body = res.stdout.trim();
20
- return body.length > 0 ? body : "(no changed files in range)";
21
- }
22
- catch {
23
- return "(could not read name-status)";
24
- }
25
- }
26
- async function gitPatchExcerpt(cwd, range) {
27
- try {
28
- const res = await runGit(cwd, ["diff", "--no-color", range]);
29
- let patch = res.stdout;
30
- if (patch.length > MAX_PATCH_CHARS) {
31
- patch =
32
- patch.slice(0, MAX_PATCH_CHARS) +
33
- `\n\n… (patch truncated at ${MAX_PATCH_CHARS} characters)\n`;
34
- }
35
- return patch.length > 0 ? patch : "(empty patch)";
36
- }
37
- catch {
38
- return "(could not read patch)";
39
- }
40
- }
41
- async function llmTaskSections(cwd, inputs) {
42
- try {
43
- const { text: raw, provider } = await llmCompleteJson([
44
- {
45
- role: "system",
46
- content: "You complete a TASK file after implementation. Reply JSON only: result (markdown, what shipped + key paths), testResult (markdown, commands run and outcomes). Be factual; use bullet lists.",
47
- },
48
- {
49
- role: "user",
50
- content: [
51
- `TASK: ${inputs.taskId} — ${inputs.taskTitle}`,
52
- "",
53
- "## Acceptance criteria",
54
- inputs.acceptanceCriteria.length > 0
55
- ? inputs.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n")
56
- : "(none listed)",
57
- "",
58
- "## Test plan (from TASK)",
59
- inputs.testPlan.length > 0 ? inputs.testPlan : "(none listed)",
60
- "",
61
- "## Git",
62
- inputs.gitBlock,
63
- "",
64
- "## Files changed",
65
- "```\n" + inputs.nameStatus + "\n```",
66
- ].join("\n"),
67
- },
68
- ], { cwd });
69
- const parsed = JSON.parse(raw);
70
- const result = String(parsed.result ?? "").trim();
71
- const testResult = String(parsed.testResult ?? "").trim();
72
- if (result.length === 0 || testResult.length === 0)
73
- return null;
74
- return { result, testResult, provider };
75
- }
76
- catch {
77
- return null;
78
- }
79
- }
80
- async function llmChangeNarrative(cwd, inputs) {
81
- try {
82
- const { text } = await llmCompleteText([
83
- {
84
- role: "system",
85
- content: "You summarize a completed software TASK for the next iteration. Write concise markdown: ## What shipped (bullets), ## Key files, ## Notes for next TASK. No fluff.",
86
- },
87
- {
88
- role: "user",
89
- content: [
90
- `TASK: ${inputs.taskId} — ${inputs.taskTitle}`,
91
- "",
92
- "## Result",
93
- inputs.result,
94
- "",
95
- "## Test Result",
96
- inputs.testResult,
97
- "",
98
- inputs.gitBlock,
99
- "",
100
- "## Files changed",
101
- "```\n" + inputs.nameStatus + "\n```",
102
- ].join("\n"),
103
- },
104
- ], { cwd });
105
- return text;
106
- }
107
- catch {
108
- return null;
109
- }
110
- }
111
- function fallbackTaskSections(inputs) {
112
- const criteriaLines = inputs.acceptanceCriteria.length > 0
113
- ? inputs.acceptanceCriteria.map((c, i) => `- [ ] ${i + 1}. ${c}`).join("\n")
114
- : "- [ ] Review Acceptance Criteria in the TASK file.";
115
- const result = [
116
- `_Auto-generated by vibeops done at ${inputs.completedAt}._`,
117
- "",
118
- "## What changed",
119
- "",
120
- inputs.gitBlock,
121
- "",
122
- "## Files touched",
123
- "",
124
- "```",
125
- inputs.nameStatus,
126
- "```",
127
- ].join("\n");
128
- const testResult = [
129
- `_Auto-generated by vibeops done at ${inputs.completedAt}._`,
130
- "",
131
- "Run your project verification commands (for example `pnpm test`, `pnpm lint`) and update this section if anything differs.",
132
- "",
133
- "## Acceptance criteria checklist",
134
- "",
135
- criteriaLines,
136
- ].join("\n");
137
- return { result, testResult };
138
- }
139
- function archiveFileName(taskId) {
140
- const safe = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
141
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
142
- return `${safe}-${stamp}.md`;
143
- }
144
- export async function buildDoneSummaryBundle(input) {
145
- const ctx = await readGitContext(input.taskFile);
146
- const body = await readText(input.taskFile);
147
- const existingResult = readSection(body, "Result");
148
- const existingTest = readSection(body, "Test Result");
149
- const acceptanceCriteria = findAcceptanceCriteria(body);
150
- const testPlan = readSection(body, "Test Plan");
151
- const completedAt = new Date().toISOString();
152
- const gitBlock = (await summarizeTaskDiff(input.cwd, input.taskFile)) ??
153
- "_No Git Context on TASK file — run `vibeops start` before the next done._";
154
- const range = ctx !== null && ctx.baseCommit.length > 0 ? `${ctx.baseCommit}..HEAD` : null;
155
- const nameStatus = range !== null ? await gitNameStatus(input.cwd, range) : "(no git range)";
156
- const patch = range !== null ? await gitPatchExcerpt(input.cwd, range) : "(no git range)";
157
- const llmProvider = await resolveAvailableLlmProvider(input.cwd);
158
- let llmProviderUsed = null;
159
- const llmSections = await llmTaskSections(input.cwd, {
160
- taskId: input.taskId,
161
- taskTitle: input.taskTitle,
162
- gitBlock,
163
- nameStatus,
164
- acceptanceCriteria,
165
- testPlan,
166
- });
167
- let resultSection;
168
- let testResultSection;
169
- if (llmSections !== null) {
170
- resultSection = llmSections.result;
171
- testResultSection = llmSections.testResult;
172
- llmProviderUsed = llmSections.provider;
173
- }
174
- else {
175
- const fallback = fallbackTaskSections({
176
- completedAt,
177
- gitBlock,
178
- nameStatus,
179
- acceptanceCriteria,
180
- });
181
- resultSection = fallback.result;
182
- testResultSection = fallback.testResult;
183
- }
184
- const narrative = await llmChangeNarrative(input.cwd, {
185
- taskId: input.taskId,
186
- taskTitle: input.taskTitle,
187
- result: resultSection,
188
- testResult: testResultSection,
189
- gitBlock,
190
- nameStatus,
191
- });
192
- if (narrative !== null && llmProviderUsed === null && llmProvider !== null) {
193
- llmProviderUsed = llmProvider;
194
- }
195
- const lines = [
196
- `# Done summary · ${input.taskId}`,
197
- "",
198
- `- **Title:** ${input.taskTitle}`,
199
- `- **Completed at:** ${completedAt}`,
200
- `- **TASK file:** \`${rel(input.cwd, input.taskFile)}\``,
201
- ctx
202
- ? `- **Git range:** \`${ctx.baseCommit}..HEAD\` (\`${ctx.taskBranch}\` → \`${ctx.baseBranch}\`)`
203
- : "",
204
- "",
205
- "## Result (TASK)",
206
- "",
207
- input.skipTaskAutoFill === true && hasNonEmptySection(body, "Result")
208
- ? existingResult
209
- : resultSection,
210
- "",
211
- "## Test Result (TASK)",
212
- "",
213
- input.skipTaskAutoFill === true && hasNonEmptySection(body, "Test Result")
214
- ? existingTest
215
- : testResultSection,
216
- "",
217
- "## Git",
218
- "",
219
- gitBlock,
220
- "",
221
- "## Files changed",
222
- "",
223
- "```",
224
- nameStatus,
225
- "```",
226
- "",
227
- ];
228
- if (narrative !== null && narrative.trim().length > 0) {
229
- const via = llmProviderUsed ?? llmProvider;
230
- const viaLabel = via === "codex-oauth"
231
- ? "Codex OAuth"
232
- : via === "cursor-agent"
233
- ? "Cursor Agent CLI"
234
- : via === "openai"
235
- ? "OpenAI API"
236
- : "LLM";
237
- lines.push(`## Change summary (${viaLabel})`, "", narrative.trim(), "");
238
- }
239
- else {
240
- lines.push("## Change summary (LLM)", "", "_(Connect Codex OAuth, Cursor Agent CLI, or OPENAI_API_KEY for a narrative; git + TASK sections above are always recorded.)_", "");
241
- }
242
- lines.push("## Patch excerpt", "", "```diff", patch, "```", "", "---", "", "_Used by `vibeops next` and the next `vibeops plan` as prior iteration context._");
243
- return {
244
- markdown: lines.join("\n"),
245
- resultSection,
246
- testResultSection,
247
- };
248
- }
249
- export async function applyTaskDoneSections(taskFile, sections, opts) {
250
- const body = await readText(taskFile);
251
- let updated = false;
252
- const writeResult = opts.refresh === true || !hasNonEmptySection(body, "Result");
253
- const writeTest = opts.refresh === true || !hasNonEmptySection(body, "Test Result");
254
- if (writeResult) {
255
- if (opts.dryRun === true) {
256
- log.info(dim(" dry-run: would update TASK → Result"));
257
- }
258
- else {
259
- await updateTaskSection(taskFile, "Result", sections.result);
260
- log.ok("TASK → Result (auto-filled)");
261
- }
262
- updated = true;
263
- }
264
- if (writeTest) {
265
- if (opts.dryRun === true) {
266
- log.info(dim(" dry-run: would update TASK → Test Result"));
267
- }
268
- else {
269
- await updateTaskSection(taskFile, "Test Result", sections.testResult);
270
- log.ok("TASK → Test Result (auto-filled)");
271
- }
272
- updated = true;
273
- }
274
- return updated;
275
- }
276
- /** Write summary files + auto-fill TASK Result / Test Result when empty. */
277
- export async function writeDoneSummary(input) {
278
- const bundle = await buildDoneSummaryBundle(input);
279
- let taskSectionsUpdated = false;
280
- if (input.skipTaskAutoFill !== true) {
281
- taskSectionsUpdated = await applyTaskDoneSections(input.taskFile, { result: bundle.resultSection, testResult: bundle.testResultSection }, { dryRun: input.dryRun, refresh: input.refreshTaskSections === true });
282
- }
283
- const lastPath = join(input.cwd, LAST_DONE_SUMMARY_REL);
284
- const archivePath = join(input.cwd, DONE_SUMMARIES_DIR, archiveFileName(input.taskId));
285
- const provider = await resolveAvailableLlmProvider(input.cwd);
286
- if (provider !== null) {
287
- const label = provider === "codex-oauth"
288
- ? "Codex OAuth"
289
- : provider === "cursor-agent"
290
- ? "Cursor Agent CLI"
291
- : "OpenAI API";
292
- log.info(dim(` LLM: ${label}`));
293
- }
294
- else {
295
- log.info(dim(" LLM: none (git-only TASK sections)"));
296
- }
297
- if (input.dryRun === true) {
298
- log.info(dim(` dry-run: would write ${LAST_DONE_SUMMARY_REL} and archive under done-summaries/`));
299
- return { ...bundle, lastPath, archivePath, taskSectionsUpdated };
300
- }
301
- const archiveDir = join(input.cwd, DONE_SUMMARIES_DIR);
302
- if (!(await isDirectory(archiveDir))) {
303
- await mkdir(archiveDir, { recursive: true });
304
- }
305
- await writeText(lastPath, bundle.markdown);
306
- await writeText(archivePath, bundle.markdown);
307
- log.ok(`Wrote ${cyan(LAST_DONE_SUMMARY_REL)} (+ archive)`);
308
- return { ...bundle, lastPath, archivePath, taskSectionsUpdated };
309
- }
310
- export async function readLastDoneSummary(cwd) {
311
- const path = join(cwd, LAST_DONE_SUMMARY_REL);
312
- if (!(await pathExists(path)))
313
- return null;
314
- const text = await readTextOrNull(path);
315
- return text !== null && text.trim().length > 0 ? text : null;
316
- }
317
- export function excerptDoneSummary(full, maxLines = 40) {
318
- const lines = full.split("\n");
319
- if (lines.length <= maxLines)
320
- return full;
321
- return [...lines.slice(0, maxLines), "", `… (${lines.length - maxLines} more lines in file)`].join("\n");
322
- }
323
- export function renderPriorIterationSection(priorSummary) {
324
- return `## Previous iteration (from last \`vibeops done\`)
325
-
326
- ${priorSummary.trim()}
327
-
328
- ---
329
- `;
330
- }
331
- /** @deprecated Use buildDoneSummaryBundle */
332
- export async function buildDoneSummaryMarkdown(input) {
333
- const bundle = await buildDoneSummaryBundle(input);
334
- return bundle.markdown;
335
- }