@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 +21 -1
- package/README.md +13 -7
- package/dist/cli.js +28 -4
- package/dist/commands/status.js +10 -2
- package/dist/commands/task-add.js +1 -1
- package/dist/commands/task-reship.js +192 -0
- package/dist/commands/task-ship.js +10 -10
- package/dist/commands/task-sync.js +16 -31
- package/dist/lib/git.js +4 -0
- package/dist/lib/resolve-task.js +1 -1
- package/dist/lib/task-add-llm.js +1 -1
- package/dist/lib/task-context.js +31 -15
- package/dist/lib/task-effective-status.js +62 -0
- package/dist/lib/task-lifecycle-target.js +6 -6
- package/dist/lib/task-push-pr.js +2 -1
- package/dist/lib/task-reship.js +192 -0
- package/dist/lib/task-scaffold.js +1 -1
- package/dist/lib/task-ship-llm.js +84 -3
- package/dist/lib/task-start.js +1 -1
- package/dist/lib/task.js +89 -41
- package/package.json +1 -1
- package/templates/clients/claude/CLAUDE.md +2 -2
- package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +1 -1
- package/templates/clients/cursor/.cursor/rules/03-docs-before-ship.mdc +1 -1
- package/templates/core/AGENTS.md +20 -5
- package/templates/core/docs/project/05-current-state.md +1 -1
- package/templates/core/docs/project/06-decisions.md +1 -1
- package/templates/core/docs/tasks/TASK-000-template.md +1 -1
- package/templates/skills-shared/implement-task/SKILL.md +2 -2
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`:
|
|
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 →
|
|
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:
|
|
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
|
-
|
|
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 **
|
|
105
|
-
- **`task
|
|
106
|
-
- **`task
|
|
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 →
|
|
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:
|
|
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")
|
package/dist/commands/status.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
|
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 →
|
|
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} (
|
|
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, "
|
|
22
|
-
log.ok("Status →
|
|
23
|
-
const committed = await commitDirtyWorkingTree(opts.cwd, docsCommitMessageFor(opts.taskId, "
|
|
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
|
|
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
|
|
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(`
|
|
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 →
|
|
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("
|
|
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 {
|
|
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 {
|
|
10
|
+
import { getMergeRequestStateForTask, isMergeRequestMerged, } from "../lib/task-effective-status.js";
|
|
11
11
|
import { resolveLifecycleTarget } from "../lib/task-lifecycle-target.js";
|
|
12
|
-
import {
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 === "
|
|
74
|
-
log.warn(`${yellow("
|
|
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);
|
package/dist/lib/resolve-task.js
CHANGED
|
@@ -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
|
|
24
|
+
if (onBranch !== null && onBranch.status === "in_progress") {
|
|
25
25
|
return { taskId: onBranch.id, taskFile: onBranch.filePath };
|
|
26
26
|
}
|
|
27
27
|
return null;
|
package/dist/lib/task-add-llm.js
CHANGED
|
@@ -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("
|
|
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.`;
|