@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,14 +1,9 @@
1
- import { ghAuthStatus, runGh } from "./github-cli.js";
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*[·:\-]\s*/i, "").trim() || taskId;
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, mode };
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, mode };
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, mode };
30
+ return { ok: false };
71
31
  }
72
- const ahead = await gitCommitsAhead(cwd, baseBranch, taskBranch);
73
- if (ahead === 0) {
74
- log.warn(`No commits on ${taskBranch} ahead of ${baseBranch}. Merge may be a no-op.`);
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
- const planLines = [
77
- `push ${remote} ${taskBranch}`,
78
- mode === "direct"
79
- ? `checkout ${baseBranch} · pull · merge --no-ff ${taskBranch} · push ${remote} ${baseBranch}`
80
- : `gh pr create (if needed) · gh pr merge --merge --delete-branch`,
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
- return { ok: true, mode };
49
+ log.warn("Dirty tree is governance-only — proceeding.");
90
50
  }
91
- log.step(bold(`Merging ${cyan(taskBranch)} → ${cyan(baseBranch)} (${mode})`));
51
+ log.step(bold(`Merging ${cyan(taskBranch)} → ${cyan(baseBranch)}`));
92
52
  try {
93
- log.step(`Pushing ${taskBranch} to ${remote}…`);
94
- await gitPush(cwd, remote, taskBranch, true);
95
- log.ok(`pushed ${taskBranch}`);
96
- }
97
- catch (e) {
98
- const msg = e instanceof Error ? e.message : String(e);
99
- log.error(`git push failed: ${msg}`);
100
- return { ok: false, mode };
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
- if (await gitBranchExists(cwd, taskBranch)) {
63
+ await gitCheckout(cwd, baseBranch);
64
+ if (hasRemote) {
159
65
  try {
160
- await gitDeleteBranch(cwd, taskBranch, { force: true });
161
- log.ok(`deleted local branch ${taskBranch}`);
66
+ await runGit(cwd, ["pull", remote, baseBranch]);
162
67
  }
163
68
  catch {
164
- log.warn(`Could not delete local branch ${taskBranch}.`);
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
- await gitPush(cwd, remote, baseBranch, false);
191
- log.ok(`merged and pushed ${baseBranch}`);
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(`merge failed: ${msg}`);
196
- log.info(dim(` Resolve conflicts on ${baseBranch}, then commit and push manually.`));
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(`deleted local branch ${taskBranch}`);
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
- try {
213
- await runGit(cwd, ["push", remote, "--delete", taskBranch]);
214
- log.ok(`deleted remote branch ${remote}/${taskBranch}`);
215
- }
216
- catch {
217
- log.warn(dim(`Remote branch may already be gone. To remove: git push ${remote} --delete ${taskBranch}`));
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, mode };
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 buildWorkNowTaskMarkdown(draft) {
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 backlog items and broad refactors not required for this slice.
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 from the repo or commands in Test Plan.
48
- 2. Result and Test Result sections are filled before \`vibeops done\`.
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
- - \`vibeops task check ${draft.id}\`
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
- ## Implementation Plan
54
+ ## Git Context
72
55
 
73
- 1. Confirm Scope with the open parent TASK (if any).
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, explicitTitle) {
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
- const cut = oneLine.slice(0, 77).trimEnd();
109
- return `${cut}...`;
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
+ }