@goodtek/vibeops 2.0.0 → 2.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to VibeOps are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.1.1 - 2026-06-08
8
+
9
+ ### Changed
10
+
11
+ - **Docs:** living markdown, templates, and Cursor rules aligned with 2.1 lifecycle (`task reship`, two-status model, sync does not edit TASK md).
12
+ - **`task sync` CLI help:** description no longer says "mark Done".
13
+
14
+ ## 2.1.0 - 2026-06-03
15
+
16
+ ### Added
17
+
18
+ - **`vibeops task reship [TASK-NNN]`** — follow-up on a **Shipped** TASK: integrate develop, archive prior MR, new MR/PR; Status stays **Shipped**.
19
+
20
+ ### Changed
21
+
22
+ - **TASK md status:** only **In Progress** → **Shipped** (`task ship`). Merge (CLI/UI) and **`task sync`** do not edit the TASK file.
23
+ - **`task sync`**: Git cleanup only (integration pull, delete `task/*` branches).
24
+ - **`task add`**: blocks only another **In Progress** TASK (not **Shipped**).
25
+ - Legacy statuses (Review, Done, Merged, Planned, …) normalize to **In Progress** or **Shipped** when read.
26
+
7
27
  ## 2.0.0 - 2026-06-03
8
28
 
9
29
  ### Breaking
@@ -18,7 +38,7 @@ All notable changes to VibeOps are documented here.
18
38
 
19
39
  ### Changed
20
40
 
21
- - **`task ship`** replaces `task done`: Done status and `doneAt` are set on **`task sync`** on the integration branch, not at ship time.
41
+ - **`task ship`** replaces `task done`: Review on the task branch at ship; **Done** is derived from merged MR (see Unreleased — sync no longer writes TASK md).
22
42
  - **`status`** next hints: ship → merge → sync.
23
43
  - Templates and Cursor rules reference ship / merge / sync (rule file `03-docs-before-ship.mdc`).
24
44
 
package/README.md CHANGED
@@ -13,9 +13,10 @@ VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on
13
13
  |---------|---------|
14
14
  | `vibeops init` | Core docs + agent packs (cursor / claude / codex) |
15
15
  | `vibeops task add` | New `TASK-NNN` file + task branch |
16
- | `vibeops task ship` | LLM summary, commit, push, open MR/PR (Status → Review) |
16
+ | `vibeops task ship` | LLM summary, commit, push, open MR/PR (Status → **Shipped**) |
17
+ | `vibeops task reship` | Shipped TASK follow-up: integrate develop, new MR/PR (Status stays Shipped) |
17
18
  | `vibeops task merge` | Merge TASK MR/PR into integration branch (default: squash) |
18
- | `vibeops task sync` | After merge: update integration branch, mark Done, delete task branch |
19
+ | `vibeops task sync` | After merge: ff-only integration pull, delete task branches (no TASK md edits) |
19
20
  | `vibeops task release` | Release PR: integration → production (GitFlow) |
20
21
  | `vibeops status` | Briefing: TASK, Git, LLM, clients |
21
22
  | `vibeops llm` | Connect LLM providers (`connect` · `status` · `use`) |
@@ -52,6 +53,9 @@ vibeops task add
52
53
 
53
54
  vibeops task ship
54
55
  vibeops task merge
56
+ # Follow-up on same TASK (after merge):
57
+ vibeops task reship TASK-NNN
58
+ vibeops task merge
55
59
  vibeops task sync
56
60
  vibeops task add
57
61
  ```
@@ -62,11 +66,11 @@ Occasionally (GitFlow release to production):
62
66
  vibeops task release
63
67
  ```
64
68
 
65
- If a TASK is **In Progress** or **Review**, `task add` exits with a guide.
69
+ Only one TASK **In Progress** at a time (`task add` blocks otherwise). **Shipped** slices do not block the next add; merge on the host or with `task merge`, then optional `task sync`.
66
70
 
67
71
  ## LLM (optional)
68
72
 
69
- For **`task add`** / **`task ship`** only (not for coding in the IDE):
73
+ For **`task add`** / **`task ship`** / **`task reship`** only (not for coding in the IDE):
70
74
 
71
75
  ```bash
72
76
  vibeops llm connect
@@ -92,6 +96,7 @@ pnpm install && pnpm build && pnpm smoke
92
96
  - **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--allow-no-remote`, `--cwd`
93
97
  - **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
94
98
  - **`task ship`**: `--dry-run`, `--no-pr`
99
+ - **`task reship`**: `--dry-run`, `--no-pr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`, `--allow-open-mr`, `--allow-dirty`
95
100
  - **`task merge`**: `--dry-run`, `--merge`, `--rebase`
96
101
  - **`task sync`**: `--dry-run`, `--no-remote-delete`, `--force`
97
102
  - **`task release`**: `--dry-run`, `--no-merge`, `--merge`, `--rebase`
@@ -101,9 +106,10 @@ pnpm install && pnpm build && pnpm smoke
101
106
 
102
107
  - **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
103
108
  - **`task add`**: `task/<slug>` from **integration** (e.g. `develop`).
104
- - **`task ship`**: commit → push → MR/PR → Status **Review** on task branch.
105
- - **`task merge`**: merge MR/PR into integration (CLI default: squash).
106
- - **`task sync`**: integration ff-only pull → Status **Done** → delete `task/*` branches.
109
+ - **`task ship`**: commit → push → MR/PR → Status **Shipped** on task branch.
110
+ - **`task reship`**: Shipped TASK only; integrates `develop`; opens **new** PR (archives previous MR in Git Context).
111
+ - **`task merge`**: merge MR/PR into integration (CLI or host UI; TASK md unchanged).
112
+ - **`task sync`**: integration ff-only pull → delete `task/*` branches (TASK md unchanged).
107
113
  - **`task release`**: integration → production PR + merge (skipped on trunk policy).
108
114
  - No force-push to shared branches.
109
115
 
package/dist/cli.js CHANGED
@@ -7,13 +7,14 @@ import { taskAddCommand } from "./commands/task-add.js";
7
7
  import { taskMergeCommand } from "./commands/task-merge.js";
8
8
  import { taskReleaseCommand } from "./commands/task-release.js";
9
9
  import { taskShipCommand } from "./commands/task-ship.js";
10
+ import { taskReshipCommand } from "./commands/task-reship.js";
10
11
  import { taskSyncCommand } from "./commands/task-sync.js";
11
12
  import { loadVibeopsEnv } from "./lib/env.js";
12
13
  import { VERSION } from "./version.js";
13
14
  const program = new Command();
14
15
  program
15
16
  .name("vibeops")
16
- .description("VibeOps — TASK workflow (init · task add/ship/merge/sync · status · llm)")
17
+ .description("VibeOps — TASK workflow (init · task add/ship/reship/merge/sync · status · llm)")
17
18
  .version(VERSION, "-v, --version", "Print the VibeOps version");
18
19
  program.hook("preAction", async (_thisCommand, actionCommand) => {
19
20
  const opts = actionCommand.opts();
@@ -56,7 +57,7 @@ program
56
57
  gitHost: opts.gitHost,
57
58
  });
58
59
  });
59
- const task = program.command("task").description("TASK lifecycle (add · ship · merge · sync · release)");
60
+ const task = program.command("task").description("TASK lifecycle (add · ship · reship · merge · sync · release)");
60
61
  task
61
62
  .command("add")
62
63
  .description("Create next TASK-NNN, task branch, In Progress")
@@ -74,7 +75,7 @@ task
74
75
  });
75
76
  task
76
77
  .command("ship [taskRef]")
77
- .description("Submit TASK: LLM summary, commit, push, open MR/PR (Status → Review)")
78
+ .description("Submit TASK: LLM summary, commit, push, open MR/PR (Status → Shipped)")
78
79
  .option("--dry-run", "Plan only")
79
80
  .option("--no-pr", "Push only; skip creating MR/PR")
80
81
  .option("--cwd <path>", "Target directory")
@@ -85,6 +86,29 @@ task
85
86
  cwd: opts.cwd,
86
87
  });
87
88
  });
89
+ task
90
+ .command("reship [taskRef]")
91
+ .description("Follow-up on Shipped TASK: integrate develop, commit, new MR/PR")
92
+ .option("--dry-run", "Plan only")
93
+ .option("--no-pr", "Push only; skip creating MR/PR")
94
+ .option("--no-integrate", "Skip merging integration branch into task branch")
95
+ .option("--recreate-branch", "Recreate task branch from integration if missing")
96
+ .option("--skip-llm", "Do not run LLM for Result / Test Result")
97
+ .option("--allow-open-mr", "Allow reship while the recorded MR is still open")
98
+ .option("--allow-dirty", "Proceed when only governance paths are dirty")
99
+ .option("--cwd <path>", "Target directory")
100
+ .action(async (taskRef, opts) => {
101
+ await taskReshipCommand(taskRef, {
102
+ dryRun: Boolean(opts.dryRun),
103
+ noPr: Boolean(opts.noPr),
104
+ noIntegrate: Boolean(opts.noIntegrate),
105
+ recreateBranch: Boolean(opts.recreateBranch),
106
+ skipLlm: Boolean(opts.skipLlm),
107
+ allowOpenMr: Boolean(opts.allowOpenMr),
108
+ allowDirty: Boolean(opts.allowDirty),
109
+ cwd: opts.cwd,
110
+ });
111
+ });
88
112
  task
89
113
  .command("merge [taskRef]")
90
114
  .description("Merge TASK MR/PR into integration branch (default: squash)")
@@ -102,7 +126,7 @@ task
102
126
  });
103
127
  task
104
128
  .command("sync [taskRef]")
105
- .description("After MR merge: update integration branch, mark Done, delete task branch")
129
+ .description("After MR merge: integration pull and delete task branch (TASK md unchanged)")
106
130
  .option("--dry-run", "Plan only")
107
131
  .option("--no-remote-delete", "Keep the task branch on the remote")
108
132
  .option("--force", "Delete local task branch with -D if not fully merged")
@@ -8,7 +8,8 @@ import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
8
8
  import { projectPaths } from "../lib/paths.js";
9
9
  import { VERSION } from "../version.js";
10
10
  import { computeNextHint, hintToText, listInProgressTasks, pickFocusTask, relPath, taskBranchExistsFor, } from "../lib/task-context.js";
11
- import { countTasks, hasNonEmptySection, isOnTaskBranch, loadActionableTasks, readGitContext, statusDisplay } from "../lib/task.js";
11
+ import { isMergeRequestMerged, taskNeedsSync } from "../lib/task-effective-status.js";
12
+ import { countTasks, hasNonEmptySection, isOnTaskBranch, loadActionableTasks, readGitContext, statusDisplay, } from "../lib/task.js";
12
13
  import { summarizeGoal } from "../lib/task-summary.js";
13
14
  async function buildReport(cwd) {
14
15
  const paths = projectPaths(cwd);
@@ -32,6 +33,8 @@ async function buildReport(cwd) {
32
33
  let branchExists = null;
33
34
  let goalExcerpt = "";
34
35
  let mergeRequestUrl = null;
36
+ let mergeRequestMerged = false;
37
+ let needsSync = false;
35
38
  if (focus !== null) {
36
39
  const body = await readText(focus.filePath);
37
40
  goalExcerpt = summarizeGoal(body);
@@ -42,6 +45,8 @@ async function buildReport(cwd) {
42
45
  mergeRequestUrl = ctx?.mergeRequestUrl ?? null;
43
46
  onTaskBranch = isOnTaskBranch(git.branch, ctx);
44
47
  branchExists = await taskBranchExistsFor(cwd, focus);
48
+ mergeRequestMerged = await isMergeRequestMerged(cwd, focus);
49
+ needsSync = await taskNeedsSync(cwd, focus);
45
50
  focusBlock = {
46
51
  id: focus.id,
47
52
  title: focus.title,
@@ -63,6 +68,9 @@ async function buildReport(cwd) {
63
68
  testFilled,
64
69
  onTaskBranch,
65
70
  hasMergeRequest: typeof mergeRequestUrl === "string" && mergeRequestUrl.length > 0,
71
+ mergeRequestMerged,
72
+ needsSync,
73
+ hasLocalChanges: git.dirty === true,
66
74
  });
67
75
  const llm = isVibeopsProject ? await buildLlmStatusReport(cwd) : null;
68
76
  return {
@@ -105,7 +113,7 @@ function printHuman(report) {
105
113
  }
106
114
  if (report.counts) {
107
115
  const c = report.counts;
108
- log.info(` ${dim("Tasks")} ${c.total} total · ${cyan(String(c.in_progress))} in progress · ${c.done} done`);
116
+ log.info(` ${dim("Tasks")} ${c.total} total · ${cyan(String(c.in_progress))} in progress · ${c.shipped} shipped`);
109
117
  }
110
118
  if (report.inProgress.length > 1) {
111
119
  log.blank();
@@ -44,7 +44,7 @@ export async function taskAddCommand(opts = {}) {
44
44
  }
45
45
  log.info(` ${dim("file")} ${relPath(root, blocking.filePath)}`);
46
46
  log.blank();
47
- log.info(`Finish it first: ${cyan(`vibeops task sync ${blocking.id}`)} (after ship + merge)`);
47
+ log.info(`Finish it first: ${cyan(`vibeops task ship ${blocking.id}`)}.`);
48
48
  log.info(`Then run ${cyan("vibeops task add")} again.`);
49
49
  process.exitCode = 1;
50
50
  return;
@@ -0,0 +1,192 @@
1
+ import { resolve } from "node:path";
2
+ import { readText } from "../lib/filesystem.js";
3
+ import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
4
+ import { projectPaths } from "../lib/paths.js";
5
+ import { relPath } from "../lib/task-context.js";
6
+ import { commitDirtyWorkingTree, docsCommitMessageFor, featCommitMessageFor, pushBranch, } from "../lib/task-git-commit.js";
7
+ import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
8
+ import { applyTaskShipMemory, llmCompleteTaskReship, writeTaskResultSections, } from "../lib/task-ship-llm.js";
9
+ import { assertMergeRequestNotOpen, ensureTaskBranchForReship, integrateIntegrationIntoTaskBranch, loadGitConfigForReship, prepareGitContextForReship, refreshGitContextBaseCommit, resolveReshipTarget, } from "../lib/task-reship.js";
10
+ import { hasNonEmptySection, parseTaskFilename, readGitContext, upsertGitContext, } from "../lib/task.js";
11
+ import { listWorkingTreeRelPaths } from "../lib/git.js";
12
+ async function commitReshipMetadataAndPush(opts) {
13
+ if (opts.dryRun) {
14
+ log.info(dim(" would commit reship metadata (Status stays Shipped)"));
15
+ if (opts.push !== null) {
16
+ log.info(dim(` would git push ${opts.push.remote} ${opts.push.branch} (reship metadata)`));
17
+ }
18
+ return true;
19
+ }
20
+ const committed = await commitDirtyWorkingTree(opts.cwd, docsCommitMessageFor(opts.taskId, "reship metadata"), false);
21
+ if (!committed) {
22
+ log.info(dim("No reship metadata commit (Git Context already on disk)."));
23
+ }
24
+ if (opts.push === null)
25
+ return true;
26
+ try {
27
+ await pushBranch(opts.cwd, opts.push.remote, opts.push.branch, false);
28
+ log.ok(`Pushed reship metadata → ${opts.push.remote}/${opts.push.branch}`);
29
+ return true;
30
+ }
31
+ catch (e) {
32
+ const msg = e instanceof Error ? e.message : String(e);
33
+ log.error(`Reship metadata push failed: ${msg}`);
34
+ return false;
35
+ }
36
+ }
37
+ export async function taskReshipCommand(taskRef, options = {}) {
38
+ const cwd = resolve(options.cwd ?? process.cwd());
39
+ const paths = projectPaths(cwd);
40
+ const dryRun = options.dryRun === true;
41
+ const target = await resolveReshipTarget(paths, cwd, taskRef);
42
+ if (target === null) {
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ const { taskId, taskFile, meta, gitCtx: initialCtx } = target;
47
+ const relFile = relPath(cwd, taskFile);
48
+ log.info(bold(`vibeops task reship ${taskId}`));
49
+ log.info(` ${dim("file")} ${relFile}`);
50
+ log.info(` ${dim("branch")} ${initialCtx.taskBranch}`);
51
+ log.info(` ${dim("status")} Shipped (unchanged)`);
52
+ log.blank();
53
+ const gitCfg = dryRun
54
+ ? await loadGitConfigForReship(cwd).catch(() => null)
55
+ : await loadGitConfigForReship(cwd);
56
+ if (!dryRun && gitCfg === null) {
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ const integrationBranch = gitCfg?.integrationBranch ?? "develop";
61
+ const remote = gitCfg?.remote ?? "origin";
62
+ if (!dryRun && !(await assertMergeRequestNotOpen(cwd, taskFile, options.allowOpenMr === true))) {
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const branchOk = await ensureTaskBranchForReship({
67
+ cwd,
68
+ taskFile,
69
+ gitCtx: initialCtx,
70
+ integrationBranch,
71
+ remote,
72
+ recreateBranch: options.recreateBranch === true,
73
+ dryRun,
74
+ allowDirty: options.allowDirty === true,
75
+ });
76
+ if (!branchOk) {
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ if (options.noIntegrate !== true) {
81
+ const integrated = await integrateIntegrationIntoTaskBranch({
82
+ cwd,
83
+ integrationBranch,
84
+ remote,
85
+ dryRun,
86
+ });
87
+ if (!integrated) {
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+ }
92
+ else {
93
+ log.info(dim("Skipped integration merge (--no-integrate)."));
94
+ }
95
+ const body = await readText(taskFile);
96
+ let resultOk = hasNonEmptySection(body, "Result");
97
+ let testOk = hasNonEmptySection(body, "Test Result");
98
+ if (!options.skipLlm && !dryRun) {
99
+ log.step("LLM — follow-up Result / Test Result");
100
+ const patch = await llmCompleteTaskReship({
101
+ cwd,
102
+ taskId,
103
+ taskTitle: meta.title,
104
+ taskBody: body,
105
+ taskFileRel: relFile,
106
+ });
107
+ if (patch !== null) {
108
+ await writeTaskResultSections(taskFile, patch.result, patch.testResult);
109
+ log.ok(`Result / Test Result updated (${patch.provider})`);
110
+ const memory = await applyTaskShipMemory(cwd, patch);
111
+ for (const p of memory.updated)
112
+ log.ok(`Updated ${p}`);
113
+ for (const p of memory.skipped)
114
+ log.skip(`Skipped ${p}`);
115
+ }
116
+ else {
117
+ log.warn("LLM unavailable — ensure Result / Test Result describe this follow-up.");
118
+ }
119
+ const body2 = await readText(taskFile);
120
+ resultOk = hasNonEmptySection(body2, "Result");
121
+ testOk = hasNonEmptySection(body2, "Test Result");
122
+ }
123
+ else if (options.skipLlm) {
124
+ log.info(dim("LLM skipped (--skip-llm)."));
125
+ }
126
+ log.info(bold("Sections"));
127
+ log.info(` ${resultOk ? green("✓") : yellow("·")} Result`);
128
+ log.info(` ${testOk ? green("✓") : yellow("·")} Test Result`);
129
+ log.blank();
130
+ if (dryRun) {
131
+ log.info(bold("dry-run — would also:"));
132
+ log.info(" · archive prior MR URL; open new MR/PR (unless --no-pr)");
133
+ log.info(" · feat commit + reship metadata push");
134
+ log.blank();
135
+ log.info(`Next: ${cyan("vibeops task merge")}, then optional ${cyan("task sync")}`);
136
+ return;
137
+ }
138
+ if (!resultOk || !testOk) {
139
+ log.error("Fill Result and Test Result for this follow-up, then rerun.");
140
+ process.exitCode = 1;
141
+ return;
142
+ }
143
+ const dirtyBefore = await listWorkingTreeRelPaths(cwd);
144
+ if (dirtyBefore.length === 0) {
145
+ log.error("No changes to reship. Edit files on the task branch, then rerun.");
146
+ process.exitCode = 1;
147
+ return;
148
+ }
149
+ const parts = parseTaskFilename(taskFile);
150
+ await commitDirtyWorkingTree(cwd, featCommitMessageFor(parts.id, meta.title), false);
151
+ let gitCtx = await readGitContext(taskFile);
152
+ if (gitCtx === null) {
153
+ log.error("Git Context missing after commit.");
154
+ process.exitCode = 1;
155
+ return;
156
+ }
157
+ gitCtx = await refreshGitContextBaseCommit(taskFile, gitCtx, cwd);
158
+ gitCtx = await prepareGitContextForReship(taskFile, gitCtx);
159
+ await upsertGitContext(taskFile, gitCtx);
160
+ const prResult = await finishTaskWithPullRequest({
161
+ cwd,
162
+ taskFile,
163
+ skipPr: options.noPr === true,
164
+ forceNewMergeRequest: true,
165
+ });
166
+ if (!prResult.ok) {
167
+ log.error("Reship failed — fix push/MR, then rerun task reship.");
168
+ process.exitCode = 1;
169
+ return;
170
+ }
171
+ if (prResult.mergeRequestUrl) {
172
+ log.info(` ${dim("MR/PR")} ${prResult.mergeRequestUrl}`);
173
+ }
174
+ const gitCtxAfter = await readGitContext(taskFile);
175
+ let pushTarget = null;
176
+ if (gitCtxAfter !== null && gitCfg !== null) {
177
+ pushTarget = { remote: gitCfg.remote, branch: gitCtxAfter.taskBranch };
178
+ }
179
+ const pushed = await commitReshipMetadataAndPush({
180
+ cwd,
181
+ taskFile,
182
+ taskId: parts.id,
183
+ dryRun: false,
184
+ push: pushTarget,
185
+ });
186
+ if (!pushed) {
187
+ process.exitCode = 1;
188
+ return;
189
+ }
190
+ log.blank();
191
+ log.info(`Next: ${cyan("vibeops task merge")} (or merge in the host UI), then ${cyan("vibeops task sync")}`);
192
+ }
@@ -11,30 +11,30 @@ import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
11
11
  import { hasNonEmptySection, parseTaskFilename, readGitContext, readTaskFile, updateInlineStatus, } from "../lib/task.js";
12
12
  async function commitShipMetadataAndPush(opts) {
13
13
  if (opts.dryRun) {
14
- log.info(dim(" would set Status → Review"));
14
+ log.info(dim(" would set Status → Shipped"));
15
15
  log.info(dim(" would commit ship metadata"));
16
16
  if (opts.push !== null) {
17
- log.info(dim(` would git push ${opts.push.remote} ${opts.push.branch} (review metadata)`));
17
+ log.info(dim(` would git push ${opts.push.remote} ${opts.push.branch} (ship metadata)`));
18
18
  }
19
19
  return true;
20
20
  }
21
- await updateInlineStatus(opts.taskFile, "review");
22
- log.ok("Status → Review");
23
- const committed = await commitDirtyWorkingTree(opts.cwd, docsCommitMessageFor(opts.taskId, "ready for review"), false);
21
+ await updateInlineStatus(opts.taskFile, "shipped");
22
+ log.ok("Status → Shipped");
23
+ const committed = await commitDirtyWorkingTree(opts.cwd, docsCommitMessageFor(opts.taskId, "mark shipped"), false);
24
24
  if (!committed) {
25
- log.warn("No file changes for review metadata commit — check TASK Status on the remote branch.");
25
+ log.warn("No file changes for ship metadata commit — check TASK Status on the remote branch.");
26
26
  return true;
27
27
  }
28
28
  if (opts.push === null)
29
29
  return true;
30
30
  try {
31
31
  await pushBranch(opts.cwd, opts.push.remote, opts.push.branch, false);
32
- log.ok(`Pushed review metadata → ${opts.push.remote}/${opts.push.branch}`);
32
+ log.ok(`Pushed ship metadata → ${opts.push.remote}/${opts.push.branch}`);
33
33
  return true;
34
34
  }
35
35
  catch (e) {
36
36
  const msg = e instanceof Error ? e.message : String(e);
37
- log.error(`Review metadata push failed: ${msg}`);
37
+ log.error(`Ship metadata push failed: ${msg}`);
38
38
  log.info(dim(`Fix auth/remote, then: git push ${opts.push.remote} ${opts.push.branch}`));
39
39
  return false;
40
40
  }
@@ -105,7 +105,7 @@ export async function taskShipCommand(taskRef, options = {}) {
105
105
  log.info(" · LLM fill Result / Test Result + patch docs/project/*");
106
106
  }
107
107
  log.info(" · commit implementation, push task branch, open MR/PR (unless --no-pr)");
108
- log.info(" · Status → Review; commit metadata; push again");
108
+ log.info(" · Status → Shipped; commit metadata; push again");
109
109
  await finishTaskWithPullRequest({
110
110
  cwd,
111
111
  taskFile,
@@ -166,7 +166,7 @@ export async function taskShipCommand(taskRef, options = {}) {
166
166
  push: pushTarget,
167
167
  });
168
168
  if (!shipped) {
169
- log.error("Review metadata push failed — fix manually, then run task merge.");
169
+ log.error("Ship metadata push failed — fix manually, then merge the MR.");
170
170
  process.exitCode = 1;
171
171
  return;
172
172
  }
@@ -3,13 +3,13 @@ import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
3
3
  import { detectGitHost, mergeRequestLabel } from "../lib/git-host.js";
4
4
  import { gitBranchExists, gitDeleteBranch, gitDeleteRemoteBranch, gitFetchRemote, gitPullFastForwardOnly, gitRemoteBranchExists, gitRemoteUrl, gitSwitchToBranch, readGitInfo, } from "../lib/git.js";
5
5
  import { bold, cyan, dim, log, yellow } from "../lib/logger.js";
6
- import { getMergeRequestState, probeMergeRequestCli } from "../lib/pr-create.js";
6
+ import { probeMergeRequestCli } from "../lib/pr-create.js";
7
7
  import { projectPaths } from "../lib/paths.js";
8
8
  import { taskNotFoundMessage } from "../lib/resolve-task.js";
9
9
  import { relPath } from "../lib/task-context.js";
10
- import { commitDirtyWorkingTree, docsCommitMessageFor, pushBranch, } from "../lib/task-git-commit.js";
10
+ import { getMergeRequestStateForTask, isMergeRequestMerged, } from "../lib/task-effective-status.js";
11
11
  import { resolveLifecycleTarget } from "../lib/task-lifecycle-target.js";
12
- import { markGitContextDone, readGitContext, readTaskFile, updateInlineStatus, } from "../lib/task.js";
12
+ import { readGitContext, readTaskFile } from "../lib/task.js";
13
13
  function isProtectedBranch(name, integration, production) {
14
14
  return name === integration || name === production || name === "main" || name === "master";
15
15
  }
@@ -52,15 +52,16 @@ export async function taskSyncCommand(taskRef, options = {}) {
52
52
  log.info(` ${dim("task branch")} ${cyan(taskBranch)}`);
53
53
  log.info(` ${dim("integration")} ${integrationBranch}`);
54
54
  log.info(` ${dim("file")} ${relPath(cwd, taskFile)}`);
55
+ log.info(` ${dim("note")} ${dim("Git cleanup only — TASK md stays Shipped")}`);
55
56
  log.blank();
56
57
  const meta = await readTaskFile(taskFile);
57
58
  if (ctx?.mergeRequestUrl && !dryRun) {
58
- const remoteUrl = await gitRemoteUrl(cwd, remote);
59
- const host = remoteUrl !== null && detectGitHost(remoteUrl) !== null
60
- ? detectGitHost(remoteUrl)
61
- : gitCfg.host;
62
- if (await probeMergeRequestCli(host)) {
63
- const state = await getMergeRequestState(cwd, host, ctx.mergeRequestUrl);
59
+ const state = await getMergeRequestStateForTask(cwd, taskFile);
60
+ if (state !== null) {
61
+ const remoteUrl = await gitRemoteUrl(cwd, remote);
62
+ const host = remoteUrl !== null && detectGitHost(remoteUrl) !== null
63
+ ? detectGitHost(remoteUrl)
64
+ : gitCfg.host;
64
65
  const label = mergeRequestLabel(host);
65
66
  if (state === "open") {
66
67
  log.warn(`${yellow(label)} still open — merge with task merge or the host UI before sync.`);
@@ -69,22 +70,23 @@ export async function taskSyncCommand(taskRef, options = {}) {
69
70
  log.ok(`${label} merged.`);
70
71
  }
71
72
  }
73
+ else if (await probeMergeRequestCli(gitCfg.host)) {
74
+ log.warn("Could not read MR state — continuing with branch cleanup.");
75
+ }
72
76
  }
73
- else if (meta.status === "review") {
74
- log.warn(`${yellow("Status")} is Review — merge the MR first, then rerun task sync.`);
77
+ else if (meta.status === "shipped" && !(await isMergeRequestMerged(cwd, meta))) {
78
+ log.warn(`${yellow("MR")} not merged yet — merge on the host or \`task merge\`, then rerun task sync.`);
75
79
  }
76
80
  if (dryRun) {
77
81
  log.info(bold("dry-run — would:"));
78
82
  log.info(` · git fetch ${remote} --prune`);
79
83
  log.info(` · git switch ${integrationBranch}`);
80
84
  log.info(` · git pull --ff-only ${remote} ${integrationBranch}`);
81
- if (meta.status !== "done") {
82
- log.info(" · Status → Done on integration branch; commit; push");
83
- }
84
85
  log.info(` · git branch ${options.force === true ? "-D" : "-d"} ${taskBranch}`);
85
86
  if (options.noRemoteDelete !== true) {
86
87
  log.info(` · git push ${remote} --delete ${taskBranch} (if exists)`);
87
88
  }
89
+ log.info(dim(" · no edits to docs/tasks/*.md"));
88
90
  log.blank();
89
91
  log.info(`Next: ${cyan("vibeops task add")}`);
90
92
  return;
@@ -128,23 +130,6 @@ export async function taskSyncCommand(taskRef, options = {}) {
128
130
  else {
129
131
  log.warn(`No ${remote}/${integrationBranch} — skipped pull.`);
130
132
  }
131
- const metaOnIntegration = await readTaskFile(taskFile);
132
- if (metaOnIntegration.status !== "done") {
133
- await updateInlineStatus(taskFile, "done");
134
- await markGitContextDone(taskFile);
135
- log.ok("Status → Done");
136
- const committed = await commitDirtyWorkingTree(cwd, docsCommitMessageFor(taskId, "mark done"), false);
137
- if (committed) {
138
- try {
139
- await pushBranch(cwd, remote, integrationBranch, false);
140
- log.ok(`Pushed done metadata → ${remote}/${integrationBranch}`);
141
- }
142
- catch (e) {
143
- const msg = e instanceof Error ? e.message : String(e);
144
- log.warn(`Could not push done metadata: ${msg}`);
145
- }
146
- }
147
- }
148
133
  if (await gitBranchExists(cwd, taskBranch)) {
149
134
  try {
150
135
  await gitDeleteBranch(cwd, taskBranch, { force: options.force === true });
package/dist/lib/git.js CHANGED
@@ -505,6 +505,10 @@ export async function gitDeleteRemoteBranch(cwd, remote, branch) {
505
505
  export async function gitPullFastForwardOnly(cwd, remote, branch) {
506
506
  await runGit(cwd, ["pull", "--ff-only", remote, branch]);
507
507
  }
508
+ /** Merge `remote/branch` into the current HEAD (follow-up integration). */
509
+ export async function gitMergeRemoteBranch(cwd, remote, branch) {
510
+ await runGit(cwd, ["merge", `${remote}/${branch}`, "--no-edit"]);
511
+ }
508
512
  export async function gitPush(cwd, remote, branch, setUpstream = true) {
509
513
  const args = setUpstream ? ["push", "-u", remote, branch] : ["push", remote, branch];
510
514
  await runGit(cwd, args);
@@ -21,7 +21,7 @@ export async function resolveTask(paths, cwd, taskRef) {
21
21
  return { taskId: inProgress.id, taskFile: inProgress.filePath };
22
22
  }
23
23
  const onBranch = await pickActiveTask(paths.docsTasks, cwd);
24
- if (onBranch !== null && onBranch.status !== "done") {
24
+ if (onBranch !== null && onBranch.status === "in_progress") {
25
25
  return { taskId: onBranch.id, taskFile: onBranch.filePath };
26
26
  }
27
27
  return null;
@@ -40,7 +40,7 @@ function systemPrompt() {
40
40
  }
41
41
 
42
42
  markdown must include sections: Status, Goal, Scope, Out of Scope, Acceptance Criteria, Test Plan, Git Context, Result, Test Result.
43
- Status body: ${statusDisplay("planned")}
43
+ Status body: ${statusDisplay("in_progress")}
44
44
  Result and Test Result: (not yet)
45
45
  Git Context: (populated by vibeops task add)
46
46
  Use the exact TASK id given in the user message for the # heading.`;