@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
package/dist/lib/task-merge.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { gitBranchExists, gitCheckout, gitCommitsAhead, gitDeleteBranch, gitGovernanceOnlyDirty, gitRemoteUrl, readGitInfo, runGit, } from "./git.js";
|
|
1
|
+
import { gitBranchExists, gitCheckout, gitDeleteBranch, gitGovernanceOnlyDirty, gitRemoteUrl, readGitInfo, runGit, } from "./git.js";
|
|
3
2
|
import { bold, cyan, dim, log } from "./logger.js";
|
|
4
3
|
function mergeCommitMessage(taskId, taskTitle, taskBranch) {
|
|
5
|
-
const slug = taskTitle.replace(/^TASK-\d+\s*[
|
|
4
|
+
const slug = taskTitle.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim() || taskId;
|
|
6
5
|
return `merge(${taskId.toLowerCase()}): ${slug} (${taskBranch})`;
|
|
7
6
|
}
|
|
8
|
-
function prTitle(taskId, taskTitle) {
|
|
9
|
-
const slug = taskTitle.replace(/^TASK-\d+\s*[·:\-]\s*/i, "").trim() || taskId;
|
|
10
|
-
return `feat(${taskId.toLowerCase()}): ${slug}`;
|
|
11
|
-
}
|
|
12
7
|
async function gitPush(cwd, remote, ref, setUpstream) {
|
|
13
8
|
const args = ["push"];
|
|
14
9
|
if (setUpstream)
|
|
@@ -16,170 +11,64 @@ async function gitPush(cwd, remote, ref, setUpstream) {
|
|
|
16
11
|
args.push(remote, ref);
|
|
17
12
|
await runGit(cwd, args);
|
|
18
13
|
}
|
|
19
|
-
async function findOpenPrNumber(baseBranch, headBranch) {
|
|
20
|
-
const res = await runGh([
|
|
21
|
-
"pr",
|
|
22
|
-
"list",
|
|
23
|
-
"--head",
|
|
24
|
-
headBranch,
|
|
25
|
-
"--base",
|
|
26
|
-
baseBranch,
|
|
27
|
-
"--state",
|
|
28
|
-
"open",
|
|
29
|
-
"--json",
|
|
30
|
-
"number,url",
|
|
31
|
-
"--limit",
|
|
32
|
-
"1",
|
|
33
|
-
]);
|
|
34
|
-
if (!res.ok)
|
|
35
|
-
return null;
|
|
36
|
-
try {
|
|
37
|
-
const rows = JSON.parse(res.stdout);
|
|
38
|
-
const row = rows[0];
|
|
39
|
-
if (row && typeof row.number === "number") {
|
|
40
|
-
return { number: row.number, url: row.url };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
14
|
export async function mergeTaskBranch(opts) {
|
|
49
|
-
const mode = opts.mode ?? "direct";
|
|
50
15
|
const remote = opts.remote ?? "origin";
|
|
51
16
|
const { cwd, gitCtx, taskId, taskTitle } = opts;
|
|
52
17
|
const { baseBranch, taskBranch } = gitCtx;
|
|
53
18
|
if (baseBranch === taskBranch) {
|
|
54
19
|
log.error("Task branch matches base branch; nothing to merge.");
|
|
55
|
-
return { ok: false
|
|
20
|
+
return { ok: false };
|
|
56
21
|
}
|
|
57
22
|
const git = await readGitInfo(cwd);
|
|
58
23
|
if (!git.isRepo) {
|
|
59
24
|
log.error("Not a git repository.");
|
|
60
|
-
return { ok: false
|
|
61
|
-
}
|
|
62
|
-
const remoteUrl = await gitRemoteUrl(cwd, remote);
|
|
63
|
-
if (!remoteUrl) {
|
|
64
|
-
log.error(`No git remote "${remote}". Run ${cyan("vibeops github init")} or add the remote first.`);
|
|
65
|
-
return { ok: false, mode };
|
|
25
|
+
return { ok: false };
|
|
66
26
|
}
|
|
67
27
|
const branchExists = await gitBranchExists(cwd, taskBranch);
|
|
68
28
|
if (!branchExists) {
|
|
69
29
|
log.error(`Local task branch not found: ${taskBranch}`);
|
|
70
|
-
return { ok: false
|
|
30
|
+
return { ok: false };
|
|
71
31
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
32
|
+
const remoteUrl = await gitRemoteUrl(cwd, remote);
|
|
33
|
+
const hasRemote = remoteUrl !== null;
|
|
34
|
+
if (opts.dryRun) {
|
|
35
|
+
log.info(bold("dry-run — would merge:"));
|
|
36
|
+
log.info(` · checkout ${baseBranch}`);
|
|
37
|
+
log.info(` · merge --no-ff ${taskBranch}`);
|
|
38
|
+
if (hasRemote)
|
|
39
|
+
log.info(` · push ${remote} (optional)`);
|
|
40
|
+
log.info(` · delete local ${taskBranch}`);
|
|
41
|
+
return { ok: true };
|
|
75
42
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`delete local branch ${taskBranch}`,
|
|
82
|
-
mode === "direct" ? `delete remote branch ${remote}/${taskBranch}` : "(remote branch removed by gh if supported)",
|
|
83
|
-
];
|
|
84
|
-
if (opts.dryRun === true) {
|
|
85
|
-
log.info(bold("dry-run — would perform:"));
|
|
86
|
-
for (const line of planLines) {
|
|
87
|
-
log.info(` · ${line}`);
|
|
43
|
+
if (git.dirty === true && opts.allowDirty !== true) {
|
|
44
|
+
const gov = await gitGovernanceOnlyDirty(cwd);
|
|
45
|
+
if (!gov.onlyGovernance) {
|
|
46
|
+
log.error("Working tree is dirty. Commit or use --allow-dirty.");
|
|
47
|
+
return { ok: false };
|
|
88
48
|
}
|
|
89
|
-
|
|
49
|
+
log.warn("Dirty tree is governance-only — proceeding.");
|
|
90
50
|
}
|
|
91
|
-
log.step(bold(`Merging ${cyan(taskBranch)} → ${cyan(baseBranch)}
|
|
51
|
+
log.step(bold(`Merging ${cyan(taskBranch)} → ${cyan(baseBranch)}`));
|
|
92
52
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
if (mode === "pr") {
|
|
103
|
-
const auth = await ghAuthStatus();
|
|
104
|
-
if (!auth.authenticated) {
|
|
105
|
-
log.error(`gh is not authenticated. Run ${cyan("gh auth login")} or use direct merge (default).`);
|
|
106
|
-
return { ok: false, mode };
|
|
107
|
-
}
|
|
108
|
-
let pr = await findOpenPrNumber(baseBranch, taskBranch);
|
|
109
|
-
if (!pr) {
|
|
110
|
-
log.step("Opening pull request…");
|
|
111
|
-
const create = await runGh([
|
|
112
|
-
"pr",
|
|
113
|
-
"create",
|
|
114
|
-
"--base",
|
|
115
|
-
baseBranch,
|
|
116
|
-
"--head",
|
|
117
|
-
taskBranch,
|
|
118
|
-
"--title",
|
|
119
|
-
prTitle(taskId, taskTitle),
|
|
120
|
-
"--body",
|
|
121
|
-
`Automated via vibeops next for ${taskId}.`,
|
|
122
|
-
]);
|
|
123
|
-
if (!create.ok) {
|
|
124
|
-
log.error(`gh pr create failed: ${create.stderr.trim() || create.stdout.trim()}`);
|
|
125
|
-
return { ok: false, mode };
|
|
53
|
+
if (hasRemote) {
|
|
54
|
+
try {
|
|
55
|
+
await gitPush(cwd, remote, taskBranch, true);
|
|
56
|
+
await runGit(cwd, ["fetch", remote]);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
60
|
+
log.warn(`Push skipped (${msg}). Continuing with local merge.`);
|
|
126
61
|
}
|
|
127
|
-
const url = create.stdout.trim();
|
|
128
|
-
log.ok(`PR created${url.length > 0 ? `: ${url}` : ""}`);
|
|
129
|
-
pr = await findOpenPrNumber(baseBranch, taskBranch);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
log.ok(`Using existing PR #${pr.number}`);
|
|
133
|
-
}
|
|
134
|
-
if (!pr) {
|
|
135
|
-
log.error("Could not find PR number after create.");
|
|
136
|
-
return { ok: false, mode };
|
|
137
|
-
}
|
|
138
|
-
log.step(`Merging PR #${pr.number}…`);
|
|
139
|
-
const merged = await runGh([
|
|
140
|
-
"pr",
|
|
141
|
-
"merge",
|
|
142
|
-
String(pr.number),
|
|
143
|
-
"--merge",
|
|
144
|
-
"--delete-branch",
|
|
145
|
-
]);
|
|
146
|
-
if (!merged.ok) {
|
|
147
|
-
log.error(`gh pr merge failed: ${merged.stderr.trim() || merged.stdout.trim()}`);
|
|
148
|
-
return { ok: false, mode };
|
|
149
|
-
}
|
|
150
|
-
log.ok(`PR #${pr.number} merged`);
|
|
151
|
-
try {
|
|
152
|
-
await gitCheckout(cwd, baseBranch);
|
|
153
|
-
await runGit(cwd, ["pull", remote, baseBranch]);
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
log.warn(`Run manually: git switch ${baseBranch} && git pull ${remote} ${baseBranch}`);
|
|
157
62
|
}
|
|
158
|
-
|
|
63
|
+
await gitCheckout(cwd, baseBranch);
|
|
64
|
+
if (hasRemote) {
|
|
159
65
|
try {
|
|
160
|
-
await
|
|
161
|
-
log.ok(`deleted local branch ${taskBranch}`);
|
|
66
|
+
await runGit(cwd, ["pull", remote, baseBranch]);
|
|
162
67
|
}
|
|
163
68
|
catch {
|
|
164
|
-
log.warn(`Could not
|
|
69
|
+
log.warn(dim(`Could not pull ${remote}/${baseBranch} — merging locally.`));
|
|
165
70
|
}
|
|
166
71
|
}
|
|
167
|
-
return { ok: true, mode, prUrl: pr.url };
|
|
168
|
-
}
|
|
169
|
-
// direct merge
|
|
170
|
-
if (git.dirty === true && opts.allowDirty !== true) {
|
|
171
|
-
const gov = await gitGovernanceOnlyDirty(cwd);
|
|
172
|
-
if (!gov.onlyGovernance) {
|
|
173
|
-
log.error("Working tree is dirty. Commit or stash before merging, or rerun with --allow-dirty.");
|
|
174
|
-
return { ok: false, mode };
|
|
175
|
-
}
|
|
176
|
-
log.warn("Dirty tree has only governance docs — proceeding.");
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
await runGit(cwd, ["fetch", remote]);
|
|
180
|
-
await gitCheckout(cwd, baseBranch);
|
|
181
|
-
await runGit(cwd, ["pull", remote, baseBranch]);
|
|
182
|
-
log.step(`Merging ${taskBranch} into ${baseBranch}…`);
|
|
183
72
|
await runGit(cwd, [
|
|
184
73
|
"merge",
|
|
185
74
|
"--no-ff",
|
|
@@ -187,34 +76,40 @@ export async function mergeTaskBranch(opts) {
|
|
|
187
76
|
"-m",
|
|
188
77
|
mergeCommitMessage(taskId, taskTitle, taskBranch),
|
|
189
78
|
]);
|
|
190
|
-
|
|
191
|
-
|
|
79
|
+
if (hasRemote) {
|
|
80
|
+
try {
|
|
81
|
+
await gitPush(cwd, remote, baseBranch, false);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
log.warn(dim(`Merge commit created locally; push ${remote} ${baseBranch} manually.`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
log.ok(`Merged into ${baseBranch}`);
|
|
192
88
|
}
|
|
193
89
|
catch (e) {
|
|
194
90
|
const msg = e instanceof Error ? e.message : String(e);
|
|
195
|
-
log.error(`
|
|
196
|
-
|
|
197
|
-
return { ok: false, mode };
|
|
91
|
+
log.error(`Merge failed: ${msg}`);
|
|
92
|
+
return { ok: false };
|
|
198
93
|
}
|
|
199
94
|
try {
|
|
200
95
|
await gitDeleteBranch(cwd, taskBranch, { force: false });
|
|
201
|
-
log.ok(`
|
|
96
|
+
log.ok(`Deleted local branch ${taskBranch}`);
|
|
202
97
|
}
|
|
203
98
|
catch {
|
|
204
99
|
try {
|
|
205
100
|
await gitDeleteBranch(cwd, taskBranch, { force: true });
|
|
206
|
-
log.ok(`deleted local branch ${taskBranch} (force)`);
|
|
207
101
|
}
|
|
208
102
|
catch {
|
|
209
103
|
log.warn(`Could not delete local branch ${taskBranch}.`);
|
|
210
104
|
}
|
|
211
105
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
106
|
+
if (hasRemote) {
|
|
107
|
+
try {
|
|
108
|
+
await runGit(cwd, ["push", remote, "--delete", taskBranch]);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
log.warn(dim(`Remote branch ${remote}/${taskBranch} may already be gone.`));
|
|
112
|
+
}
|
|
218
113
|
}
|
|
219
|
-
return { ok: true
|
|
114
|
+
return { ok: true };
|
|
220
115
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { basename, join } from "node:path";
|
|
2
2
|
import { statusDisplay } from "./task.js";
|
|
3
|
+
export const V3_TASK_SECTIONS = [
|
|
4
|
+
"Status",
|
|
5
|
+
"Goal",
|
|
6
|
+
"Scope",
|
|
7
|
+
"Out of Scope",
|
|
8
|
+
"Acceptance Criteria",
|
|
9
|
+
"Test Plan",
|
|
10
|
+
"Git Context",
|
|
11
|
+
"Result",
|
|
12
|
+
"Test Result",
|
|
13
|
+
];
|
|
3
14
|
export function allocateNextTaskNumber(tasks) {
|
|
4
15
|
let max = 0;
|
|
5
16
|
for (const t of tasks) {
|
|
@@ -12,67 +23,37 @@ export function allocateNextTaskNumber(tasks) {
|
|
|
12
23
|
export function formatTaskId(number) {
|
|
13
24
|
return `TASK-${String(number).padStart(3, "0")}`;
|
|
14
25
|
}
|
|
15
|
-
export function
|
|
16
|
-
const spawnedNote = draft.spawnedFrom
|
|
17
|
-
? `\n\nWork-now slice while **${draft.spawnedFrom}** is still open — not a pre-planned backlog item scheduled after later TASK numbers.`
|
|
18
|
-
: "";
|
|
26
|
+
export function buildMinimalTaskMarkdown(draft) {
|
|
19
27
|
return `# ${draft.id}: ${draft.title}
|
|
20
28
|
|
|
21
29
|
## Status
|
|
22
30
|
|
|
23
31
|
${statusDisplay("planned")}
|
|
24
32
|
|
|
25
|
-
## MVP Phase
|
|
26
|
-
|
|
27
|
-
${draft.mvpPhase}
|
|
28
|
-
|
|
29
33
|
## Goal
|
|
30
34
|
|
|
31
35
|
${draft.idea.trim()}
|
|
32
36
|
|
|
33
|
-
## Background
|
|
34
|
-
|
|
35
|
-
${draft.idea.trim()}${spawnedNote}
|
|
36
|
-
|
|
37
37
|
## Scope
|
|
38
38
|
|
|
39
39
|
- Deliver the goal above in a single focused pass.
|
|
40
40
|
|
|
41
41
|
## Out of Scope
|
|
42
42
|
|
|
43
|
-
- Unrelated
|
|
43
|
+
- Unrelated refactors and features outside this slice.
|
|
44
44
|
|
|
45
45
|
## Acceptance Criteria
|
|
46
46
|
|
|
47
|
-
1. Goal and Scope are met and verifiable
|
|
48
|
-
2. Result and Test Result
|
|
49
|
-
|
|
50
|
-
## Files to Inspect First
|
|
51
|
-
|
|
52
|
-
- (add paths before implementation)
|
|
53
|
-
|
|
54
|
-
## Expected Files to Change
|
|
55
|
-
|
|
56
|
-
- (list as you implement)
|
|
57
|
-
|
|
58
|
-
## Risks
|
|
59
|
-
|
|
60
|
-
- Scope creep from the parent TASK — keep this slice narrow.
|
|
47
|
+
1. Goal and Scope are met and verifiable via Test Plan.
|
|
48
|
+
2. Result and Test Result are filled before \`vibeops task done\`.
|
|
61
49
|
|
|
62
50
|
## Test Plan
|
|
63
51
|
|
|
64
|
-
-
|
|
65
|
-
- Project-specific checks for the change
|
|
66
|
-
|
|
67
|
-
## Rollback Plan
|
|
68
|
-
|
|
69
|
-
- \`vibeops rollback ${draft.id}\` (advisory) or revert the task branch commits.
|
|
52
|
+
- Project-specific checks for this change
|
|
70
53
|
|
|
71
|
-
##
|
|
54
|
+
## Git Context
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
2. Implement on \`vibeops start ${draft.id}\`.
|
|
75
|
-
3. Fill Result and Test Result, then \`vibeops done ${draft.id}\`.
|
|
56
|
+
(populated by \`vibeops task add\`)
|
|
76
57
|
|
|
77
58
|
## Result
|
|
78
59
|
|
|
@@ -98,13 +79,28 @@ export function uniqueTaskPath(tasksDir, taskId, baseSlug, existingPaths) {
|
|
|
98
79
|
}
|
|
99
80
|
return { slug, filePath: join(tasksDir, name) };
|
|
100
81
|
}
|
|
101
|
-
export function titleFromIdea(idea
|
|
102
|
-
const t = explicitTitle?.trim();
|
|
103
|
-
if (t && t.length > 0)
|
|
104
|
-
return t;
|
|
82
|
+
export function titleFromIdea(idea) {
|
|
105
83
|
const oneLine = idea.replace(/\s+/g, " ").trim();
|
|
106
84
|
if (oneLine.length <= 80)
|
|
107
85
|
return oneLine;
|
|
108
|
-
|
|
109
|
-
|
|
86
|
+
return `${oneLine.slice(0, 77).trimEnd()}...`;
|
|
87
|
+
}
|
|
88
|
+
export function ensureV3Sections(markdown) {
|
|
89
|
+
let body = markdown;
|
|
90
|
+
for (const section of V3_TASK_SECTIONS) {
|
|
91
|
+
const re = new RegExp(`^##\\s+${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "im");
|
|
92
|
+
if (!re.test(body)) {
|
|
93
|
+
body = `${body.trimEnd()}\n\n## ${section}\n\n(not yet)\n`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return body;
|
|
97
|
+
}
|
|
98
|
+
export function normalizeTaskHeader(markdown, taskId, title) {
|
|
99
|
+
const lines = markdown.split("\n");
|
|
100
|
+
const header = `# ${taskId}: ${title}`;
|
|
101
|
+
if (lines.length > 0 && /^#\s+/.test(lines[0])) {
|
|
102
|
+
lines[0] = header;
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
return `${header}\n\n${markdown}`;
|
|
110
106
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
|
+
import { branchNameForTaskFile, readGitContext, readTaskFile, updateInlineStatus, upsertGitContext, } from "./task.js";
|
|
3
|
+
import { detectDefaultBranch, gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitGovernanceOnlyDirty, gitHeadCommit, readGitInfo, } from "./git.js";
|
|
4
|
+
import { cyan, dim, log } from "./logger.js";
|
|
5
|
+
function relOrAbs(root, p) {
|
|
6
|
+
const r = relative(root, p);
|
|
7
|
+
return r === "" ? "." : r.startsWith("..") ? p : r;
|
|
8
|
+
}
|
|
9
|
+
/** Create or resume task branch and mark TASK In Progress (used by `task add`). */
|
|
10
|
+
export async function startTaskBranch(opts) {
|
|
11
|
+
const cwd = resolve(opts.cwd);
|
|
12
|
+
const taskFile = opts.taskFile;
|
|
13
|
+
const meta = await readTaskFile(taskFile);
|
|
14
|
+
const git = await readGitInfo(cwd);
|
|
15
|
+
if (!git.isRepo) {
|
|
16
|
+
if (opts.dryRun) {
|
|
17
|
+
log.info(dim("dry-run — would require a git repository"));
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
log.error("Not a git repository. Run `vibeops init --git` first.");
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (git.dirty === true && opts.allowDirty !== true) {
|
|
24
|
+
const gov = await gitGovernanceOnlyDirty(cwd);
|
|
25
|
+
if (!gov.onlyGovernance) {
|
|
26
|
+
log.error("Git working tree is dirty. Commit or stash, or rerun with --allow-dirty.");
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
log.warn("Only governance paths are dirty — proceeding.");
|
|
30
|
+
}
|
|
31
|
+
const baseBranch = git.branch ?? (await detectDefaultBranch(cwd)) ?? "main";
|
|
32
|
+
const baseCommit = (await gitHeadCommit(cwd)) ?? "";
|
|
33
|
+
if (baseCommit.length === 0) {
|
|
34
|
+
log.error("No commits on HEAD yet. Create an initial commit first.");
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const taskBranch = branchNameForTaskFile(taskFile);
|
|
38
|
+
const ctx = {
|
|
39
|
+
baseBranch,
|
|
40
|
+
baseCommit,
|
|
41
|
+
taskBranch,
|
|
42
|
+
startedAt: new Date().toISOString(),
|
|
43
|
+
};
|
|
44
|
+
const existingCtx = await readGitContext(taskFile);
|
|
45
|
+
const branchExists = await gitBranchExists(cwd, taskBranch);
|
|
46
|
+
log.info(` ${dim("task branch")} ${cyan(taskBranch)}`);
|
|
47
|
+
log.info(` ${dim("base")} ${baseBranch} @ ${baseCommit.slice(0, 7)}`);
|
|
48
|
+
if (opts.dryRun) {
|
|
49
|
+
log.info(dim(branchExists
|
|
50
|
+
? `dry-run — would git switch ${taskBranch}`
|
|
51
|
+
: `dry-run — would git switch -c ${taskBranch}`));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (branchExists) {
|
|
55
|
+
await gitCheckout(cwd, taskBranch);
|
|
56
|
+
if (meta.status === "planned") {
|
|
57
|
+
await updateInlineStatus(taskFile, "in_progress");
|
|
58
|
+
}
|
|
59
|
+
if (existingCtx === null) {
|
|
60
|
+
await upsertGitContext(taskFile, ctx);
|
|
61
|
+
}
|
|
62
|
+
log.ok(`Resumed ${taskBranch}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await gitCheckoutNewBranch(cwd, taskBranch, baseBranch);
|
|
66
|
+
await updateInlineStatus(taskFile, "in_progress");
|
|
67
|
+
await upsertGitContext(taskFile, ctx);
|
|
68
|
+
log.ok(`Branch ${taskBranch} ready`);
|
|
69
|
+
}
|
|
70
|
+
log.info(` ${dim("file")} ${relOrAbs(cwd, taskFile)}`);
|
|
71
|
+
return true;
|
|
72
|
+
}
|