@goodtek/vibeops 2.4.3 → 2.5.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 +20 -0
- package/README.md +10 -10
- package/dist/cli.js +9 -29
- package/dist/commands/init.js +43 -21
- package/dist/commands/status.js +91 -72
- package/dist/commands/task-merge.js +23 -5
- package/dist/commands/task-release.js +19 -9
- package/dist/commands/task-sync.js +5 -3
- package/dist/lib/git-host-cli.js +115 -0
- package/dist/lib/git-remote.js +177 -59
- package/dist/lib/merge-request-readiness.js +57 -0
- package/dist/lib/pr-create.js +103 -12
- package/dist/lib/task-context.js +28 -16
- package/dist/lib/task-push-pr.js +6 -2
- package/dist/lib/task-reship.js +1 -1
- package/package.json +14 -12
- package/templates/core/AGENTS.md +1 -1
- package/templates/core/docs/project/06-decisions.md +1 -1
- package/dist/commands/task-reship.js +0 -21
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.5.0 - 2026-07-19
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- **Removed `vibeops task reship`.** Use `vibeops task ship` (state-aware). After merge, confirm or pass `--new-cycle`. Hidden flag `ship --reship` remains as an alias for `--new-cycle`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`vibeops init` remote setup:** ask GitHub / GitLab / Skip, then Create or Connect. Soft-gate when `gh`/`glab` is missing (install hints, optional `brew install` with consent, or manual URL).
|
|
16
|
+
- **`vibeops status`:** human output is a **Now / Next** card (focus, checklist, PR, next command); details (project, counts, LLM) move to a dim footer.
|
|
17
|
+
- Shared **host CLI install/login hints** for ship / merge / release when the tool is missing.
|
|
18
|
+
|
|
19
|
+
## 2.4.4 - 2026-06-25
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **`task sync`:** After merge verification passes, delete the local `task/*` branch with `-D`. Squash-merged TASK branches no longer require `--force` for cleanup.
|
|
24
|
+
- **`task merge` / `task release`:** Wait for MR/PR CI to finish before calling the host merge API. GitLab no longer gets an immediate `--auto-merge=false` merge while the pipeline is still running (405). Falls back to auto-merge when needed, then polls until `merged_at` is set.
|
|
25
|
+
- **`task release`:** Default merge strategy is now **merge commit** (integration → production). Use `--squash` to squash.
|
|
26
|
+
|
|
7
27
|
## 2.4.3 - 2026-06-25
|
|
8
28
|
|
|
9
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -15,22 +15,23 @@ VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on
|
|
|
15
15
|
| `vibeops task add` | New `TASK-NNN` file + task branch |
|
|
16
16
|
| `vibeops task del` | Cancel TASK before merge (md + branch + close open MR) |
|
|
17
17
|
| `vibeops task ship` | State-aware submit: open a new PR · update the open PR · start a new PR cycle after merge (Status → **Shipped**) |
|
|
18
|
-
| `vibeops task reship` | **Deprecated** alias for `task ship --new-cycle` |
|
|
19
18
|
| `vibeops task merge` | Merge TASK MR/PR into integration branch (default: squash) |
|
|
20
19
|
| `vibeops task sync` | After merge: ff-only integration pull, delete task branches (no TASK md edits) |
|
|
21
20
|
| `vibeops task release` | Release PR: integration → production (GitFlow) |
|
|
22
21
|
| `vibeops pull` | Fetch remote + update integration branch (e.g. develop) |
|
|
23
|
-
| `vibeops status` |
|
|
22
|
+
| `vibeops status` | Now / Next briefing: focus TASK, Git, what to run next |
|
|
24
23
|
| `vibeops llm` | Connect LLM providers (`connect` · `status` · `use`) |
|
|
25
24
|
|
|
26
25
|
## Init
|
|
27
26
|
|
|
27
|
+
Interactive init asks which agents to install, Git branch policy, then **where the remote lives** (GitHub / GitLab / skip) and whether to **create** or **connect** a repository. If `gh`/`glab` is missing, init prints install hints (optional `brew install` with consent) or accepts a URL — it does not block the rest of VibeOps.
|
|
28
|
+
|
|
28
29
|
```bash
|
|
29
|
-
# Interactive: pick agents (≥1), then Git
|
|
30
|
+
# Interactive: pick agents (≥1), then Git + remote host
|
|
30
31
|
vibeops init
|
|
31
32
|
|
|
32
33
|
# Non-interactive (GitFlow: develop + main, origin required unless CI)
|
|
33
|
-
vibeops init --clients cursor,claude,codex --git --initial-commit --git-policy gitflow
|
|
34
|
+
vibeops init --clients cursor,claude,codex --git --initial-commit --git-policy gitflow --allow-no-remote
|
|
34
35
|
|
|
35
36
|
# Re-init templates on an existing project
|
|
36
37
|
vibeops init --clients cursor --yes
|
|
@@ -107,11 +108,10 @@ pnpm install && pnpm build && pnpm smoke
|
|
|
107
108
|
|
|
108
109
|
## Flags (common)
|
|
109
110
|
|
|
110
|
-
- **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--allow-no-remote`, `--cwd`
|
|
111
|
+
- **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--git-host github|gitlab`, `--allow-no-remote`, `--cwd`
|
|
111
112
|
- **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
|
|
112
113
|
- **`task del`**: `--dry-run`, `--force`, `--no-remote-delete`, `--no-close-mr`
|
|
113
|
-
- **`task ship`**: `-m/--message`, `--new-cycle
|
|
114
|
-
- **`task reship`** (deprecated): `--dry-run`, `--no-pr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`, `--allow-open-mr` → delegates to `task ship --new-cycle`
|
|
114
|
+
- **`task ship`**: `-m/--message`, `--new-cycle`, `--no-commit`, `--dry-run`, `--no-pr`, `--non-interactive`, `--allow-open-mr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`
|
|
115
115
|
- **`task merge`**: `--dry-run`, `--merge`, `--rebase`
|
|
116
116
|
- **`task sync`**: `--dry-run`, `--no-remote-delete`, `--force`
|
|
117
117
|
- **`task release`**: `--dry-run`, `--no-merge`, `--merge`, `--rebase`
|
|
@@ -120,14 +120,14 @@ pnpm install && pnpm build && pnpm smoke
|
|
|
120
120
|
|
|
121
121
|
## Git
|
|
122
122
|
|
|
123
|
-
- **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
|
|
123
|
+
- **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`). Interactive remote setup: ask GitHub/GitLab, create or connect; soft-gate when `gh`/`glab` is missing.
|
|
124
124
|
- **`task add`**: `task/<slug>` from **integration** (e.g. `develop`).
|
|
125
|
-
- **`task ship`** (state-aware): **no PR** → commit + ship metadata (Status **Shipped**) → push → open MR/PR; **open PR** → commit + push the same branch, CI re-runs, no new PR; **merged PR** → new PR cycle (carries uncommitted work onto the task branch, integrates `develop`, opens a **new** PR). Commit messages are TASK-id-scoped (`feat(task-001): …`). Refuses when HEAD is not the task branch.
|
|
126
|
-
- **`task reship`** *(deprecated)*: alias for `task ship --new-cycle`.
|
|
125
|
+
- **`task ship`** (state-aware): **no PR** → commit + ship metadata (Status **Shipped**) → push → open MR/PR; **open PR** → commit + push the same branch, CI re-runs, no new PR; **merged PR** → new PR cycle (carries uncommitted work onto the task branch, integrates `develop`, opens a **new** PR). Use `--new-cycle` in non-interactive mode. Commit messages are TASK-id-scoped (`feat(task-001): …`). Refuses when HEAD is not the task branch.
|
|
127
126
|
- **`task merge`**: merge MR/PR into integration (CLI or host UI; TASK md unchanged).
|
|
128
127
|
- **`task sync`**: integration ff-only pull → delete `task/*` branches (TASK md unchanged).
|
|
129
128
|
- **`pull`**: fetch + switch to integration branch + `git pull --ff-only` (one command).
|
|
130
129
|
- **`task release`**: integration → production PR + merge (skipped on trunk policy).
|
|
130
|
+
- **`status`**: Now / Next card — focus stage, checklist, PR, and the next command to run.
|
|
131
131
|
- No force-push to shared branches.
|
|
132
132
|
|
|
133
133
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
3
|
import { pullCommand } from "./commands/pull.js";
|
|
4
4
|
import { initCommand } from "./commands/init.js";
|
|
5
5
|
import { llmConnectCommand, llmStatusCommand, llmUseCommand } from "./commands/llm.js";
|
|
@@ -9,7 +9,6 @@ import { taskDelCommand } from "./commands/task-del.js";
|
|
|
9
9
|
import { taskMergeCommand } from "./commands/task-merge.js";
|
|
10
10
|
import { taskReleaseCommand } from "./commands/task-release.js";
|
|
11
11
|
import { taskShipCommand } from "./commands/task-ship.js";
|
|
12
|
-
import { taskReshipCommand } from "./commands/task-reship.js";
|
|
13
12
|
import { taskSyncCommand } from "./commands/task-sync.js";
|
|
14
13
|
import { loadVibeopsEnv } from "./lib/env.js";
|
|
15
14
|
import { VERSION } from "./version.js";
|
|
@@ -61,7 +60,7 @@ program
|
|
|
61
60
|
});
|
|
62
61
|
const task = program
|
|
63
62
|
.command("task")
|
|
64
|
-
.description("TASK lifecycle (add · del · ship ·
|
|
63
|
+
.description("TASK lifecycle (add · del · ship · merge · sync · release)");
|
|
65
64
|
task
|
|
66
65
|
.command("add")
|
|
67
66
|
.description("Create next TASK-NNN, task branch, In Progress")
|
|
@@ -99,8 +98,8 @@ task
|
|
|
99
98
|
.description("State-aware submit: new PR · update open PR · new PR cycle after merge (Status → Shipped)")
|
|
100
99
|
.option("--dry-run", "Plan only")
|
|
101
100
|
.option("-m, --message <msg>", "Commit message (TASK id auto-prefixed; LLM/prompt when omitted)")
|
|
102
|
-
.option("--new-cycle", "Allow a new PR cycle after merge without prompting
|
|
103
|
-
.
|
|
101
|
+
.option("--new-cycle", "Allow a new PR cycle after merge without prompting")
|
|
102
|
+
.addOption(new Option("--reship", "Alias for --new-cycle").hideHelp())
|
|
104
103
|
.option("--no-commit", "Push already-committed changes only (skip staging/commit)")
|
|
105
104
|
.option("--no-pr", "Push only; skip creating MR/PR")
|
|
106
105
|
.option("--non-interactive", "CI: never prompt")
|
|
@@ -124,27 +123,6 @@ task
|
|
|
124
123
|
cwd: opts.cwd,
|
|
125
124
|
});
|
|
126
125
|
});
|
|
127
|
-
task
|
|
128
|
-
.command("reship [taskRef]")
|
|
129
|
-
.description("Deprecated alias for `task ship --new-cycle` (start a new PR cycle after merge)")
|
|
130
|
-
.option("--dry-run", "Plan only")
|
|
131
|
-
.option("--no-pr", "Push only; skip creating MR/PR")
|
|
132
|
-
.option("--no-integrate", "Skip merging integration branch into task branch")
|
|
133
|
-
.option("--recreate-branch", "Create task branch from integration instead of reusing local/remote ref")
|
|
134
|
-
.option("--skip-llm", "Do not run LLM for Result / Test Result")
|
|
135
|
-
.option("--allow-open-mr", "Update the open MR/PR with new commits (merge before reship for a new PR)")
|
|
136
|
-
.option("--cwd <path>", "Target directory")
|
|
137
|
-
.action(async (taskRef, opts) => {
|
|
138
|
-
await taskReshipCommand(taskRef, {
|
|
139
|
-
dryRun: Boolean(opts.dryRun),
|
|
140
|
-
noPr: opts.pr === false,
|
|
141
|
-
noIntegrate: opts.integrate === false,
|
|
142
|
-
recreateBranch: Boolean(opts.recreateBranch),
|
|
143
|
-
skipLlm: Boolean(opts.skipLlm),
|
|
144
|
-
allowOpenMr: Boolean(opts.allowOpenMr),
|
|
145
|
-
cwd: opts.cwd,
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
126
|
task
|
|
149
127
|
.command("merge [taskRef]")
|
|
150
128
|
.description("Merge TASK MR/PR into integration branch (default: squash)")
|
|
@@ -165,7 +143,7 @@ task
|
|
|
165
143
|
.description("After MR merge: integration pull and delete task branch (TASK md unchanged)")
|
|
166
144
|
.option("--dry-run", "Plan only")
|
|
167
145
|
.option("--no-remote-delete", "Keep the task branch on the remote")
|
|
168
|
-
.option("--force", "Bypass MR merge
|
|
146
|
+
.option("--force", "Bypass MR merge verification before branch cleanup")
|
|
169
147
|
.option("--cwd <path>", "Target directory")
|
|
170
148
|
.action(async (taskRef, opts) => {
|
|
171
149
|
await taskSyncCommand(taskRef, {
|
|
@@ -177,10 +155,11 @@ task
|
|
|
177
155
|
});
|
|
178
156
|
task
|
|
179
157
|
.command("release")
|
|
180
|
-
.description("Open (and merge) release MR/PR: integration branch → production")
|
|
158
|
+
.description("Open (and merge) release MR/PR: integration branch → production (default: merge commit)")
|
|
181
159
|
.option("--dry-run", "Plan only")
|
|
182
160
|
.option("--no-merge", "Create release PR only")
|
|
183
|
-
.option("--
|
|
161
|
+
.option("--squash", "Squash merge (default is merge commit for release)")
|
|
162
|
+
.option("--merge", "Merge commit (default for release)")
|
|
184
163
|
.option("--rebase", "Rebase merge")
|
|
185
164
|
.option("--cwd <path>", "Target directory")
|
|
186
165
|
.action(async (opts) => {
|
|
@@ -188,6 +167,7 @@ task
|
|
|
188
167
|
dryRun: Boolean(opts.dryRun),
|
|
189
168
|
noMerge: Boolean(opts.noMerge),
|
|
190
169
|
merge: Boolean(opts.merge),
|
|
170
|
+
squash: Boolean(opts.squash),
|
|
191
171
|
rebase: Boolean(opts.rebase),
|
|
192
172
|
cwd: opts.cwd,
|
|
193
173
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -108,30 +108,41 @@ async function resolveBranchPolicy(options, interactive) {
|
|
|
108
108
|
}
|
|
109
109
|
return { integrationBranch: "develop", productionBranch: "main" };
|
|
110
110
|
}
|
|
111
|
-
function printNextSteps(clients, git) {
|
|
111
|
+
function printNextSteps(clients, git, opts) {
|
|
112
112
|
log.blank();
|
|
113
113
|
log.info("Next steps:");
|
|
114
|
-
|
|
114
|
+
let n = 1;
|
|
115
|
+
log.info(` ${n++}. Read AGENTS.md`);
|
|
115
116
|
if (clients.includes("cursor")) {
|
|
116
|
-
log.info(
|
|
117
|
+
log.info(` ${n++}. Cursor: @docs/tasks/TASK-001-….md in Ask, then Agent (+ /plan-task, /implement-task)`);
|
|
117
118
|
}
|
|
118
|
-
if (clients.includes("claude")) {
|
|
119
|
-
log.info(
|
|
119
|
+
else if (clients.includes("claude")) {
|
|
120
|
+
log.info(` ${n++}. Claude Code: open TASK file; use /plan-task then /implement-task`);
|
|
120
121
|
}
|
|
121
|
-
if (clients.includes("codex")) {
|
|
122
|
-
log.info(
|
|
122
|
+
else if (clients.includes("codex")) {
|
|
123
|
+
log.info(` ${n++}. Codex: open TASK file; use $plan-task / $implement-task when needed`);
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
-
log.info(
|
|
125
|
+
else {
|
|
126
|
+
log.info(` ${n++}. Open the current TASK file in your agent before coding`);
|
|
127
|
+
}
|
|
128
|
+
log.info(` ${n++}. ${cyan("vibeops llm connect")} — LLM for task add / task ship`);
|
|
129
|
+
if (!opts.hasRemote) {
|
|
130
|
+
log.info(` ${n++}. Add a remote later: ${dim(`git remote add ${git.remote} <url>`)} (or re-run init)`);
|
|
131
|
+
}
|
|
132
|
+
else if (!opts.pushedBaseline) {
|
|
133
|
+
log.info(` ${n++}. Push branches to origin (first time only):`);
|
|
134
|
+
log.info(` ${dim(`git push -u ${git.remote} ${git.productionBranch}`)}`);
|
|
135
|
+
if (git.integrationBranch !== git.productionBranch) {
|
|
136
|
+
log.info(` ${dim(`git push -u ${git.remote} ${git.integrationBranch}`)}`);
|
|
137
|
+
}
|
|
126
138
|
}
|
|
127
|
-
log.info(`
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
log.info(` ${n++}. ${cyan("vibeops task add")} — branches from ${git.integrationBranch} (pulls latest first)`);
|
|
140
|
+
if (opts.hasRemote) {
|
|
141
|
+
log.info(` ${n++}. ${cyan("vibeops task ship")} → merge → sync — TASK lifecycle on ${git.host}`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
log.info(` ${n++}. ${cyan("vibeops task ship")} — after origin exists`);
|
|
132
145
|
}
|
|
133
|
-
log.info(` 5. ${cyan("vibeops task add")} — branches from ${git.integrationBranch} (pulls latest first)`);
|
|
134
|
-
log.info(` 6. ${cyan("vibeops task ship")} → merge → sync — TASK lifecycle on ${git.host}`);
|
|
135
146
|
}
|
|
136
147
|
export async function initCommand(options = {}) {
|
|
137
148
|
const dryRun = options.dryRun === true;
|
|
@@ -223,6 +234,7 @@ export async function initCommand(options = {}) {
|
|
|
223
234
|
dryRun,
|
|
224
235
|
nonInteractive: !interactive,
|
|
225
236
|
allowMissing: options.allowNoRemote === true,
|
|
237
|
+
preferredHost: hostFromFlag ?? existingConfig?.git?.host,
|
|
226
238
|
defaultBranch: gitConfig.productionBranch,
|
|
227
239
|
});
|
|
228
240
|
if (remote !== null) {
|
|
@@ -232,14 +244,17 @@ export async function initCommand(options = {}) {
|
|
|
232
244
|
host: remote.host,
|
|
233
245
|
};
|
|
234
246
|
}
|
|
247
|
+
let pushedBaseline = hadRemoteBefore;
|
|
235
248
|
// If we created/configured a new remote during init, push baseline branches so
|
|
236
249
|
// `task add` can always pull latest integration before branching.
|
|
237
|
-
if (!dryRun &&
|
|
250
|
+
if (!dryRun &&
|
|
251
|
+
options.allowNoRemote !== true &&
|
|
252
|
+
!hadRemoteBefore &&
|
|
253
|
+
remote !== null) {
|
|
238
254
|
const remoteName = gitConfig.remote;
|
|
239
255
|
log.blank();
|
|
240
256
|
log.info("Remote bootstrap:");
|
|
241
257
|
try {
|
|
242
|
-
// Ensure local branches are up-to-date with origin if they already exist remotely.
|
|
243
258
|
const prodRemoteExists = await gitRemoteBranchExists(projectRoot, remoteName, gitConfig.productionBranch);
|
|
244
259
|
if (prodRemoteExists) {
|
|
245
260
|
await gitCheckout(projectRoot, gitConfig.productionBranch);
|
|
@@ -258,6 +273,7 @@ export async function initCommand(options = {}) {
|
|
|
258
273
|
await gitPush(projectRoot, remoteName, gitConfig.integrationBranch, true);
|
|
259
274
|
log.ok(`pushed ${gitConfig.integrationBranch} → ${remoteName}`);
|
|
260
275
|
}
|
|
276
|
+
pushedBaseline = true;
|
|
261
277
|
}
|
|
262
278
|
catch (e) {
|
|
263
279
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -267,14 +283,20 @@ export async function initCommand(options = {}) {
|
|
|
267
283
|
if (gitConfig.integrationBranch !== gitConfig.productionBranch) {
|
|
268
284
|
log.info(dim(` git push -u ${remoteName} ${gitConfig.integrationBranch}`));
|
|
269
285
|
}
|
|
286
|
+
pushedBaseline = false;
|
|
270
287
|
}
|
|
271
288
|
}
|
|
272
289
|
if (!dryRun) {
|
|
273
290
|
await writeConfig(projectRoot, buildConfig(name, clients, gitConfig, existingConfig));
|
|
274
|
-
if (
|
|
275
|
-
log.warn(
|
|
291
|
+
if (remote === null) {
|
|
292
|
+
log.warn(options.allowNoRemote === true
|
|
293
|
+
? "No git remote configured (--allow-no-remote). task ship push/MR will fail until origin exists."
|
|
294
|
+
: "No git remote configured. task ship push/MR will fail until origin exists.");
|
|
276
295
|
}
|
|
277
|
-
printNextSteps(clients, gitConfig
|
|
296
|
+
printNextSteps(clients, gitConfig, {
|
|
297
|
+
hasRemote: remote !== null,
|
|
298
|
+
pushedBaseline,
|
|
299
|
+
});
|
|
278
300
|
}
|
|
279
301
|
}
|
|
280
302
|
async function runGitSetup(projectRoot, plan, dryRun) {
|
package/dist/commands/status.js
CHANGED
|
@@ -7,7 +7,7 @@ import { buildLlmStatusReport } from "../lib/llm-status.js";
|
|
|
7
7
|
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
|
-
import { computeNextHint, hintToText, listInProgressTasks, pickFocusTask, relPath, taskBranchExistsFor, } from "../lib/task-context.js";
|
|
10
|
+
import { computeNextHint, hintToLines, hintToText, listInProgressTasks, pickFocusTask, relPath, taskBranchExistsFor, } from "../lib/task-context.js";
|
|
11
11
|
import { hasOpenTaskMergeRequest, isMergeRequestMerged, resolveTaskMergeRequestUrl, taskNeedsSync, } from "../lib/task-effective-status.js";
|
|
12
12
|
import { countTasks, hasNonEmptySection, isOnTaskBranch, loadActionableTasks, readGitContext, statusDisplay, } from "../lib/task.js";
|
|
13
13
|
import { summarizeGoal } from "../lib/task-summary.js";
|
|
@@ -59,6 +59,8 @@ async function buildReport(cwd) {
|
|
|
59
59
|
taskBranch,
|
|
60
60
|
taskBranchExists: branchExists,
|
|
61
61
|
mergeRequestUrl,
|
|
62
|
+
mergeRequestMerged,
|
|
63
|
+
needsSync,
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
const hint = computeNextHint({
|
|
@@ -85,6 +87,8 @@ async function buildReport(cwd) {
|
|
|
85
87
|
branch: git.branch,
|
|
86
88
|
dirty: git.dirty,
|
|
87
89
|
policy: config?.git ? formatGitPolicySummary(config.git) : null,
|
|
90
|
+
host: config?.git?.host ?? null,
|
|
91
|
+
remote: config?.git?.remote ?? null,
|
|
88
92
|
},
|
|
89
93
|
counts,
|
|
90
94
|
inProgress: inProgress.map((t) => ({
|
|
@@ -94,96 +98,111 @@ async function buildReport(cwd) {
|
|
|
94
98
|
})),
|
|
95
99
|
focus: focusBlock,
|
|
96
100
|
nextHint: hintToText(hint, cwd, focus),
|
|
101
|
+
nextLines: [...hintToLines(hint, cwd, focus)],
|
|
97
102
|
llm,
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
log.info(bold("
|
|
105
|
-
log.info(` ${dim("CLI")} v${report.vibeopsVersion}`);
|
|
105
|
+
function filledLabel(ok) {
|
|
106
|
+
return ok ? green("ok") : yellow("empty");
|
|
107
|
+
}
|
|
108
|
+
function printNow(report) {
|
|
109
|
+
log.info(bold(" NOW"));
|
|
106
110
|
if (!report.isVibeopsProject) {
|
|
107
|
-
log.info(`
|
|
108
|
-
log.info(bold("─".repeat(56)));
|
|
109
|
-
log.blank();
|
|
111
|
+
log.info(` ${dim("Project")} ${yellow("not a VibeOps project")}`);
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
112
|
-
log.info(` ${dim("Project")} ${report.projectName ?? "(unnamed)"}`);
|
|
113
|
-
if (report.clients.length > 0) {
|
|
114
|
-
log.info(` ${dim("Clients")} ${report.clients.join(", ")}`);
|
|
115
|
-
}
|
|
116
|
-
if (report.counts) {
|
|
117
|
-
const c = report.counts;
|
|
118
|
-
log.info(` ${dim("Tasks")} ${c.total} total · ${cyan(String(c.in_progress))} in progress · ${c.shipped} shipped`);
|
|
119
|
-
}
|
|
120
|
-
if (report.inProgress.length > 1) {
|
|
121
|
-
log.blank();
|
|
122
|
-
log.info(bold(" In progress"));
|
|
123
|
-
for (const t of report.inProgress) {
|
|
124
|
-
log.info(` ${cyan(t.id)} — ${t.title}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
log.blank();
|
|
128
114
|
if (report.focus) {
|
|
129
115
|
const f = report.focus;
|
|
130
|
-
log.info(`
|
|
131
|
-
log.info(`
|
|
132
|
-
log.info(`
|
|
133
|
-
if (f.
|
|
134
|
-
|
|
135
|
-
|
|
116
|
+
log.info(` ${dim("Focus")} ${cyan(f.id)} — ${f.title || dim("(no title)")}`);
|
|
117
|
+
log.info(` ${dim("Stage")} ${f.status}`);
|
|
118
|
+
log.info(` ${dim("Checklist")} Result ${filledLabel(f.resultFilled)} · Test Result ${filledLabel(f.testResultFilled)}`);
|
|
119
|
+
if (f.taskBranch) {
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (f.onTaskBranch === true)
|
|
122
|
+
parts.push("on it");
|
|
123
|
+
else if (f.onTaskBranch === false)
|
|
124
|
+
parts.push("not checked out");
|
|
125
|
+
if (report.git.dirty === true)
|
|
126
|
+
parts.push("dirty");
|
|
127
|
+
else if (report.git.dirty === false)
|
|
128
|
+
parts.push("clean");
|
|
129
|
+
const suffix = parts.length > 0 ? ` (${parts.join(" · ")})` : "";
|
|
130
|
+
log.info(` ${dim("Branch")} ${f.taskBranch}${suffix}`);
|
|
131
|
+
}
|
|
132
|
+
else if (report.git.isRepo) {
|
|
133
|
+
log.info(` ${dim("Branch")} ${report.git.branch ?? "(detached)"}${report.git.dirty === true ? yellow(" · dirty") : ""}`);
|
|
134
|
+
}
|
|
135
|
+
if (f.needsSync) {
|
|
136
|
+
log.info(` ${dim("PR")} ${dim("(merged — sync pending)")}`);
|
|
137
|
+
}
|
|
138
|
+
else if (f.mergeRequestUrl) {
|
|
139
|
+
const tag = f.mergeRequestMerged ? dim(" · merged") : "";
|
|
140
|
+
log.info(` ${dim("PR")} ${f.mergeRequestUrl}${tag}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
log.info(` ${dim("PR")} ${dim("(none)")}`);
|
|
144
|
+
}
|
|
136
145
|
}
|
|
137
146
|
else {
|
|
138
|
-
log.info(`
|
|
147
|
+
log.info(` ${dim("Focus")} ${dim("(none)")}`);
|
|
148
|
+
if (report.git.isRepo) {
|
|
149
|
+
log.info(` ${dim("Branch")} ${report.git.branch ?? "(detached)"}${report.git.dirty === true ? yellow(" · dirty") : ""}`);
|
|
150
|
+
}
|
|
139
151
|
}
|
|
152
|
+
}
|
|
153
|
+
function printNext(report) {
|
|
140
154
|
log.blank();
|
|
141
|
-
log.info(bold("
|
|
142
|
-
|
|
143
|
-
log.info(` ${
|
|
155
|
+
log.info(bold(" NEXT"));
|
|
156
|
+
for (const line of report.nextLines) {
|
|
157
|
+
log.info(` ${cyan("→")} ${line}`);
|
|
144
158
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
}
|
|
160
|
+
function printDetails(report) {
|
|
161
|
+
log.blank();
|
|
162
|
+
log.info(dim(" ──"));
|
|
163
|
+
if (!report.isVibeopsProject) {
|
|
164
|
+
log.info(` ${dim("CLI")} v${report.vibeopsVersion}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const clients = report.clients.length > 0 ? report.clients.join(", ") : dim("(none)");
|
|
168
|
+
log.info(` ${dim("Project")} ${report.projectName ?? "(unnamed)"} · clients ${clients}`);
|
|
169
|
+
if (report.counts) {
|
|
170
|
+
const c = report.counts;
|
|
171
|
+
log.info(` ${dim("Tasks")} ${c.total} total · ${cyan(String(c.in_progress))} in progress · ${c.shipped} shipped`);
|
|
172
|
+
}
|
|
173
|
+
if (report.inProgress.length > 1) {
|
|
174
|
+
log.info(` ${dim("Active")} ${report.inProgress.map((t) => t.id).join(", ")}`);
|
|
175
|
+
}
|
|
176
|
+
const hostBits = [];
|
|
177
|
+
if (report.git.host)
|
|
178
|
+
hostBits.push(report.git.host);
|
|
179
|
+
if (report.git.remote)
|
|
180
|
+
hostBits.push(report.git.remote);
|
|
181
|
+
if (report.git.policy)
|
|
182
|
+
hostBits.push(report.git.policy);
|
|
183
|
+
if (hostBits.length > 0) {
|
|
184
|
+
log.info(` ${dim("Git")} ${hostBits.join(" · ")}`);
|
|
165
185
|
}
|
|
166
186
|
if (report.llm) {
|
|
167
|
-
log.blank();
|
|
168
|
-
log.info(bold(" LLM"));
|
|
169
|
-
log.info(` ${dim("Preferred")} ${report.llm.preference}`);
|
|
170
187
|
const active = report.llm.active !== null
|
|
171
|
-
?
|
|
188
|
+
? `${report.llm.active} (ok)`
|
|
172
189
|
: report.llm.anyAvailable
|
|
173
|
-
?
|
|
174
|
-
:
|
|
175
|
-
log.info(`
|
|
176
|
-
for (const p of report.llm.providers) {
|
|
177
|
-
const summary = p.summary.length > 56 ? `${p.summary.slice(0, 53)}…` : p.summary;
|
|
178
|
-
log.info(` ${dim(p.id.padEnd(14))} ${p.ok ? green("ok") : dim("—")} ${dim(summary)}`);
|
|
179
|
-
}
|
|
180
|
-
if (!report.llm.anyAvailable) {
|
|
181
|
-
log.info(` ${dim("Setup")} ${cyan("vibeops llm connect")}`);
|
|
182
|
-
}
|
|
190
|
+
? "(preference not available)"
|
|
191
|
+
: "none";
|
|
192
|
+
log.info(` ${dim("LLM")} ${active} · prefer ${report.llm.preference}${report.llm.anyAvailable ? "" : ` · ${cyan("vibeops llm connect")}`}`);
|
|
183
193
|
}
|
|
194
|
+
}
|
|
195
|
+
function printHuman(report) {
|
|
196
|
+
log.blank();
|
|
197
|
+
log.info(bold("─".repeat(44)));
|
|
198
|
+
log.info(bold(" VibeOps · status"));
|
|
199
|
+
log.info(bold("─".repeat(44)));
|
|
200
|
+
log.blank();
|
|
201
|
+
printNow(report);
|
|
202
|
+
printNext(report);
|
|
203
|
+
printDetails(report);
|
|
184
204
|
log.blank();
|
|
185
|
-
log.info(
|
|
186
|
-
log.info(bold("─".repeat(56)));
|
|
205
|
+
log.info(bold("─".repeat(44)));
|
|
187
206
|
log.blank();
|
|
188
207
|
}
|
|
189
208
|
export async function statusCommand(opts = {}) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
|
|
3
|
+
import { formatHostCliHint, formatHostCliMissingMessage } from "../lib/git-host-cli.js";
|
|
3
4
|
import { detectGitHost, mergeRequestLabel } from "../lib/git-host.js";
|
|
4
5
|
import { gitRemoteUrl } from "../lib/git.js";
|
|
5
6
|
import { bold, cyan, dim, log } from "../lib/logger.js";
|
|
@@ -55,7 +56,7 @@ export async function taskMergeCommand(taskRef, options = {}) {
|
|
|
55
56
|
? null
|
|
56
57
|
: await resolveOpenTaskMergeRequest(cwd, target.taskFile);
|
|
57
58
|
if (!dryRun && resolvedMr === null) {
|
|
58
|
-
log.error(`No open MR/PR for ${ctx.taskBranch} → ${ctx.baseBranch}. Run task ship
|
|
59
|
+
log.error(`No open MR/PR for ${ctx.taskBranch} → ${ctx.baseBranch}. Run task ship first.`);
|
|
59
60
|
process.exitCode = 1;
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
@@ -96,13 +97,23 @@ export async function taskMergeCommand(taskRef, options = {}) {
|
|
|
96
97
|
if (state === "open") {
|
|
97
98
|
const cliOk = await probeMergeRequestCli(host);
|
|
98
99
|
if (!cliOk) {
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
log.error(formatHostCliMissingMessage(host, `merge the ${label}`));
|
|
101
|
+
for (const line of formatHostCliHint(host)) {
|
|
102
|
+
log.info(dim(` ${line}`));
|
|
103
|
+
}
|
|
104
|
+
log.info(dim("Or merge in the host UI, then task sync."));
|
|
101
105
|
process.exitCode = 1;
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
104
108
|
try {
|
|
105
|
-
await mergeMergeRequest({
|
|
109
|
+
await mergeMergeRequest({
|
|
110
|
+
cwd,
|
|
111
|
+
host,
|
|
112
|
+
url: mergeRequestUrl,
|
|
113
|
+
method,
|
|
114
|
+
waitForCi: true,
|
|
115
|
+
immediate: true,
|
|
116
|
+
});
|
|
106
117
|
const verified = await assertMergeRequestMerged({
|
|
107
118
|
cwd,
|
|
108
119
|
host,
|
|
@@ -132,7 +143,14 @@ export async function taskMergeCommand(taskRef, options = {}) {
|
|
|
132
143
|
else {
|
|
133
144
|
log.warn(`Could not read ${label} state — attempting merge anyway.`);
|
|
134
145
|
try {
|
|
135
|
-
await mergeMergeRequest({
|
|
146
|
+
await mergeMergeRequest({
|
|
147
|
+
cwd,
|
|
148
|
+
host,
|
|
149
|
+
url: mergeRequestUrl,
|
|
150
|
+
method,
|
|
151
|
+
waitForCi: true,
|
|
152
|
+
immediate: true,
|
|
153
|
+
});
|
|
136
154
|
const verified = await assertMergeRequestMerged({
|
|
137
155
|
cwd,
|
|
138
156
|
host,
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
|
|
3
|
+
import { formatHostCliHint, formatHostCliMissingMessage } from "../lib/git-host-cli.js";
|
|
3
4
|
import { detectGitHost, mergeRequestLabel } from "../lib/git-host.js";
|
|
4
5
|
import { gitRemoteUrl } from "../lib/git.js";
|
|
5
6
|
import { bold, dim, log } from "../lib/logger.js";
|
|
6
7
|
import { createMergeRequest, getMergeRequestState, mergeMergeRequest, probeMergeRequestCli, } from "../lib/pr-create.js";
|
|
7
8
|
import { assertMergeRequestMerged } from "../lib/task-merge-verify.js";
|
|
8
|
-
function
|
|
9
|
+
function resolveReleaseMergeMethod(opts) {
|
|
9
10
|
if (opts.rebase === true)
|
|
10
11
|
return "rebase";
|
|
11
|
-
if (opts.
|
|
12
|
-
return "
|
|
13
|
-
return "
|
|
12
|
+
if (opts.squash === true)
|
|
13
|
+
return "squash";
|
|
14
|
+
return "merge";
|
|
14
15
|
}
|
|
15
16
|
export async function taskReleaseCommand(options = {}) {
|
|
16
17
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
@@ -51,14 +52,16 @@ export async function taskReleaseCommand(options = {}) {
|
|
|
51
52
|
log.info(bold("dry-run — would:"));
|
|
52
53
|
log.info(` · ${label}: ${integrationBranch} → ${productionBranch}`);
|
|
53
54
|
if (options.noMerge !== true) {
|
|
54
|
-
log.info(` · merge ${label} (${
|
|
55
|
+
log.info(` · merge ${label} (${resolveReleaseMergeMethod(options)}, wait for CI)`);
|
|
55
56
|
}
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
59
|
const cliOk = await probeMergeRequestCli(host);
|
|
59
60
|
if (!cliOk) {
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
log.error(formatHostCliMissingMessage(host, `create the release ${label}`));
|
|
62
|
+
for (const line of formatHostCliHint(host)) {
|
|
63
|
+
log.info(dim(` ${line}`));
|
|
64
|
+
}
|
|
62
65
|
process.exitCode = 1;
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
@@ -90,9 +93,16 @@ export async function taskReleaseCommand(options = {}) {
|
|
|
90
93
|
log.ok(`Release ${label} already merged.`);
|
|
91
94
|
return;
|
|
92
95
|
}
|
|
93
|
-
const method =
|
|
96
|
+
const method = resolveReleaseMergeMethod(options);
|
|
94
97
|
try {
|
|
95
|
-
await mergeMergeRequest({
|
|
98
|
+
await mergeMergeRequest({
|
|
99
|
+
cwd,
|
|
100
|
+
host,
|
|
101
|
+
url: releaseUrl,
|
|
102
|
+
method,
|
|
103
|
+
waitForCi: true,
|
|
104
|
+
immediate: true,
|
|
105
|
+
});
|
|
96
106
|
const verified = await assertMergeRequestMerged({
|
|
97
107
|
cwd,
|
|
98
108
|
host,
|