@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.
- package/CHANGELOG.md +21 -1
- package/README.md +62 -382
- package/dist/bootstrap/installer.js +4 -16
- package/dist/cli.js +26 -160
- package/dist/commands/init.js +3 -3
- package/dist/commands/status.js +145 -8
- package/dist/commands/task-add.js +64 -164
- package/dist/commands/task-done.js +117 -74
- package/dist/lib/config.js +2 -118
- package/dist/lib/git.js +1 -1
- package/dist/lib/paths.js +7 -3
- package/dist/lib/plan-llm-openai.js +0 -63
- package/dist/lib/resolve-task.js +34 -0
- package/dist/lib/task-add-llm.js +46 -233
- package/dist/lib/task-context.js +68 -0
- package/dist/lib/task-done-llm.js +148 -0
- package/dist/lib/task-merge.js +54 -159
- package/dist/lib/task-scaffold.js +39 -43
- package/dist/lib/task-start.js +72 -0
- package/dist/lib/task-summary.js +12 -239
- package/dist/lib/task.js +15 -24
- package/dist/types/config.js +0 -16
- package/package.json +3 -5
- package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
- package/templates/.cursor/rules/02-git-safety.mdc +11 -0
- package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
- package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
- package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
- package/templates/AGENTS.md +26 -87
- package/templates/docs/logs/README.md +4 -35
- package/templates/docs/project/03-architecture.md +10 -29
- package/templates/docs/project/05-current-state.md +7 -27
- package/templates/docs/project/06-decisions.md +6 -17
- package/templates/docs/tasks/TASK-000-template.md +9 -44
- package/dist/agent/loader.js +0 -71
- package/dist/agent/prompt.js +0 -66
- package/dist/commands/agent-list.js +0 -32
- package/dist/commands/agent-prompt.js +0 -59
- package/dist/commands/agent-show.js +0 -26
- package/dist/commands/done.js +0 -160
- package/dist/commands/github-init.js +0 -554
- package/dist/commands/github-status.js +0 -164
- package/dist/commands/next.js +0 -283
- package/dist/commands/notion-init.js +0 -764
- package/dist/commands/notion-sync.js +0 -405
- package/dist/commands/notion-test.js +0 -595
- package/dist/commands/plan.js +0 -146
- package/dist/commands/rollback.js +0 -126
- package/dist/commands/start.js +0 -129
- package/dist/commands/task-check.js +0 -155
- package/dist/commands/task-generate.js +0 -206
- package/dist/commands/task-pull.js +0 -277
- package/dist/commands/task-rollback.js +0 -174
- package/dist/commands/task-start.js +0 -102
- package/dist/commands/task-status.js +0 -122
- package/dist/lib/brief.js +0 -356
- package/dist/lib/cursor-task-prompt.js +0 -60
- package/dist/lib/done-follow-up.js +0 -308
- package/dist/lib/github-cli.js +0 -247
- package/dist/lib/guide-state.js +0 -69
- package/dist/lib/mvp-artifacts.js +0 -204
- package/dist/lib/mvp-constants.js +0 -15
- package/dist/lib/mvp-diff-summary.js +0 -43
- package/dist/lib/mvp-done-summary.js +0 -335
- package/dist/lib/notion-client.js +0 -459
- package/dist/lib/notion-discovery.js +0 -671
- package/dist/lib/notion-env.js +0 -140
- package/dist/lib/notion-mappers.js +0 -148
- package/dist/lib/notion-schema.js +0 -272
- package/dist/lib/notion-sync.js +0 -337
- package/dist/lib/notion-target.js +0 -247
- package/dist/lib/package-json.js +0 -133
- package/dist/lib/plan-apply-planner.js +0 -131
- package/dist/lib/plan-codex-pkce.js +0 -234
- package/dist/lib/plan-llm-models.js +0 -237
- package/dist/lib/plan-llm-normalize.js +0 -146
- package/dist/lib/plan-llm-session.js +0 -682
- package/dist/lib/plan-post-apply-setup.js +0 -116
- package/dist/lib/project-docs.js +0 -95
- package/dist/lib/prompt-builder.js +0 -135
- package/dist/lib/resolve-command-task.js +0 -40
- package/dist/lib/task-add-build-prompt.js +0 -39
- package/dist/lib/task-add-cursor.js +0 -81
- package/dist/lib/task-generator.js +0 -183
- package/dist/lib/task-prompt.js +0 -23
- package/dist/lib/task-pull.js +0 -354
- package/dist/lib/task-status-brief.js +0 -132
- package/dist/lib/workflow-guide.js +0 -485
- package/dist/status/collector.js +0 -103
- package/dist/status/format.js +0 -181
- package/dist/types/brief.js +0 -126
- package/templates/.cursor/rules/00-project-governance.mdc +0 -28
- package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
- package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
- package/templates/.cursor/rules/03-git-safety.mdc +0 -30
- package/templates/.cursor/rules/04-docs-update.mdc +0 -22
- package/templates/.vibeops/agents/architect.md +0 -47
- package/templates/.vibeops/agents/builder.md +0 -38
- package/templates/.vibeops/agents/docs.md +0 -54
- package/templates/.vibeops/agents/orchestrator.md +0 -40
- package/templates/.vibeops/agents/planner.md +0 -60
- package/templates/.vibeops/agents/recovery.md +0 -49
- package/templates/.vibeops/agents/reviewer.md +0 -47
- package/templates/.vibeops/agents/tester.md +0 -43
- package/templates/.vibeops/prompts/create-plan.md +0 -33
- package/templates/.vibeops/prompts/generate-tasks.md +0 -41
- package/templates/.vibeops/prompts/implement-task.md +0 -39
- package/templates/.vibeops/prompts/review-task.md +0 -34
- package/templates/.vibeops/prompts/rollback.md +0 -32
- package/templates/.vibeops/prompts/start-project.md +0 -39
- package/templates/.vibeops/workflows/notion-sync.md +0 -53
- package/templates/.vibeops/workflows/project-start.md +0 -73
- package/templates/.vibeops/workflows/rollback.md +0 -45
- package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
- package/templates/docs/project/00-overview.md +0 -27
- package/templates/docs/project/01-requirements.md +0 -30
- package/templates/docs/project/02-mvp-scope.md +0 -36
- package/templates/docs/project/04-tech-stack.md +0 -29
- package/templates/docs/project/07-backlog.md +0 -23
- package/templates/docs/project/08-env.md +0 -29
- package/templates/docs/project/09-deployment.md +0 -28
|
@@ -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
|
-
}
|