@goodtek/vibeops 2.4.4 → 2.5.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,25 @@ All notable changes to VibeOps are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.5.1 - 2026-07-20
8
+
9
+ ### Fixed
10
+
11
+ - **`task add`:** Preflight-syncs the integration branch **before** creating a TASK file. On ff-only failure, prints a classified diagnosis (dirty / ahead / diverged) with copy-paste fix commands. Incomplete adds (file created, branch missing) resume on the next `task add` instead of blocking with “finish ship first”.
12
+ - **`vibeops pull`:** Uses the same integration sync diagnosis when `--ff-only` fails.
13
+
14
+ ## 2.5.0 - 2026-07-19
15
+
16
+ ### Breaking
17
+
18
+ - **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`.
19
+
20
+ ### Changed
21
+
22
+ - **`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).
23
+ - **`vibeops status`:** human output is a **Now / Next** card (focus, checklist, PR, next command); details (project, counts, LLM) move to a dim footer.
24
+ - Shared **host CLI install/login hints** for ship / merge / release when the tool is missing.
25
+
7
26
  ## 2.4.4 - 2026-06-25
8
27
 
9
28
  ### Fixed
package/README.md CHANGED
@@ -7,30 +7,59 @@
7
7
 
8
8
  VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on Git branches, and runs a clear GitFlow lifecycle. You plan and implement in your chosen agent; the CLI handles files, Git, and short LLM assists.
9
9
 
10
+ **Current release:** [`@goodtek/vibeops@2.5.1`](https://www.npmjs.com/package/@goodtek/vibeops) — interactive init remote (GitHub/GitLab), state-aware `task ship` only (no `task reship`), `status` Now/Next card, clearer `task add` integration sync errors.
11
+
10
12
  ## Commands
11
13
 
12
14
  | Command | Purpose |
13
15
  |---------|---------|
14
- | `vibeops init` | Core docs + agent packs (cursor / claude / codex) |
16
+ | `vibeops init` | Core docs + agent packs; interactive Git host + create/connect remote |
15
17
  | `vibeops task add` | New `TASK-NNN` file + task branch |
16
18
  | `vibeops task del` | Cancel TASK before merge (md + branch + close open MR) |
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
+ | `vibeops task ship` | State-aware submit: new PR · update open PR · new PR cycle after merge (Status → **Shipped**) |
19
20
  | `vibeops task merge` | Merge TASK MR/PR into integration branch (default: squash) |
20
21
  | `vibeops task sync` | After merge: ff-only integration pull, delete task branches (no TASK md edits) |
21
22
  | `vibeops task release` | Release PR: integration → production (GitFlow) |
22
23
  | `vibeops pull` | Fetch remote + update integration branch (e.g. develop) |
23
- | `vibeops status` | Briefing: TASK, Git, LLM, clients |
24
+ | `vibeops status` | **Now / Next** card — focus TASK, checklist, what to run next |
24
25
  | `vibeops llm` | Connect LLM providers (`connect` · `status` · `use`) |
25
26
 
27
+ ## Installation
28
+
29
+ Node.js 20+.
30
+
31
+ ```bash
32
+ npm install -g @goodtek/vibeops@2.5.1
33
+ # or latest: npm install -g @goodtek/vibeops
34
+ vibeops --version # expect 2.5.1
35
+ ```
36
+
37
+ If `vibeops --version` stays on an old release after install, check for a **shell alias** or **Volta** shim shadowing npm:
38
+
39
+ ```bash
40
+ type -a vibeops
41
+ # alias → unalias vibeops (and remove from ~/.zshrc)
42
+ # ~/.volta/bin/vibeops → volta install @goodtek/vibeops@2.5.1
43
+ ```
44
+
45
+ **Upgrading from before 2.5:** `task reship` was removed. Use `vibeops task ship` (re-run while the PR is open; after merge use confirm or `--new-cycle`).
46
+
47
+ Development:
48
+
49
+ ```bash
50
+ pnpm install && pnpm build && pnpm smoke
51
+ ```
52
+
26
53
  ## Init
27
54
 
55
+ 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.
56
+
28
57
  ```bash
29
- # Interactive: pick agents (≥1), then Git
58
+ # Interactive: pick agents (≥1), then Git + remote host
30
59
  vibeops init
31
60
 
32
61
  # Non-interactive (GitFlow: develop + main, origin required unless CI)
33
- vibeops init --clients cursor,claude,codex --git --initial-commit --git-policy gitflow
62
+ vibeops init --clients cursor,claude,codex --git --initial-commit --git-policy gitflow --allow-no-remote
34
63
 
35
64
  # Re-init templates on an existing project
36
65
  vibeops init --clients cursor --yes
@@ -82,36 +111,39 @@ vibeops task release
82
111
 
83
112
  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`.
84
113
 
85
- ## LLM (optional)
114
+ `task add` fast-forward-pulls the integration branch first. If that fails (diverged / dirty / local-ahead), it prints the cause and fix commands and **does not** create a TASK file. If a previous run left a TASK file without a branch, rerun `task add` to resume.
86
115
 
87
- For **`task add`** / **`task ship`** only (not for coding in the IDE). When `-m` is omitted, `ship` uses a connected provider to draft the commit subject:
116
+ ## Status
88
117
 
89
118
  ```bash
90
- vibeops llm connect
91
- vibeops llm use auto # auto | codex-oauth | cursor-agent | openai
119
+ vibeops status
92
120
  ```
93
121
 
94
- ## Installation
122
+ Prints a short card:
95
123
 
96
- Node.js 20+.
124
+ - **NOW** — focus TASK, stage, Result/Test checklist, branch, PR
125
+ - **NEXT** — one primary action (copy-pasteable command)
126
+ - footer — project, task counts, LLM (dim)
97
127
 
98
128
  ```bash
99
- npm install -g @goodtek/vibeops
129
+ vibeops status --json # machine-readable
100
130
  ```
101
131
 
102
- Development:
132
+ ## LLM (optional)
133
+
134
+ For **`task add`** / **`task ship`** only (not for coding in the IDE). When `-m` is omitted, `ship` uses a connected provider to draft the commit subject:
103
135
 
104
136
  ```bash
105
- pnpm install && pnpm build && pnpm smoke
137
+ vibeops llm connect
138
+ vibeops llm use auto # auto | codex-oauth | cursor-agent | openai
106
139
  ```
107
140
 
108
141
  ## Flags (common)
109
142
 
110
- - **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--allow-no-remote`, `--cwd`
143
+ - **`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
144
  - **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
112
145
  - **`task del`**: `--dry-run`, `--force`, `--no-remote-delete`, `--no-close-mr`
113
- - **`task ship`**: `-m/--message`, `--new-cycle` (alias `--reship`), `--no-commit`, `--dry-run`, `--no-pr`, `--non-interactive`, `--allow-open-mr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`
114
- - **`task reship`** (deprecated): `--dry-run`, `--no-pr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`, `--allow-open-mr` → delegates to `task ship --new-cycle`
146
+ - **`task ship`**: `-m/--message`, `--new-cycle`, `--no-commit`, `--dry-run`, `--no-pr`, `--non-interactive`, `--allow-open-mr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`
115
147
  - **`task merge`**: `--dry-run`, `--merge`, `--rebase`
116
148
  - **`task sync`**: `--dry-run`, `--no-remote-delete`, `--force`
117
149
  - **`task release`**: `--dry-run`, `--no-merge`, `--merge`, `--rebase`
@@ -120,14 +152,14 @@ pnpm install && pnpm build && pnpm smoke
120
152
 
121
153
  ## Git
122
154
 
123
- - **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
155
+ - **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
156
  - **`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`.
157
+ - **`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
158
  - **`task merge`**: merge MR/PR into integration (CLI or host UI; TASK md unchanged).
128
159
  - **`task sync`**: integration ff-only pull → delete `task/*` branches (TASK md unchanged).
129
160
  - **`pull`**: fetch + switch to integration branch + `git pull --ff-only` (one command).
130
161
  - **`task release`**: integration → production PR + merge (skipped on trunk policy).
162
+ - **`status`**: Now / Next card — focus stage, checklist, PR, and the next command to run.
131
163
  - No force-push to shared branches.
132
164
 
133
165
  ## 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 · reship · merge · sync · release)");
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 (alias: --reship)")
103
- .option("--reship", "Alias for --new-cycle")
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)")
@@ -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
- log.info(" 1. Read AGENTS.md");
114
+ let n = 1;
115
+ log.info(` ${n++}. Read AGENTS.md`);
115
116
  if (clients.includes("cursor")) {
116
- log.info(" 2. Cursor: @docs/tasks/TASK-001-….md in Ask, then Agent (+ /plan-task, /implement-task)");
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(" 2. Claude Code: open TASK file; use /plan-task then /implement-task");
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(" 2. Codex: open TASK file; use $plan-task / $implement-task when needed");
122
+ else if (clients.includes("codex")) {
123
+ log.info(` ${n++}. Codex: open TASK file; use $plan-task / $implement-task when needed`);
123
124
  }
124
- if (!clients.includes("cursor") && !clients.includes("claude") && !clients.includes("codex")) {
125
- log.info(" 2. Open the current TASK file in your agent before coding");
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(` 3. ${cyan("vibeops llm connect")} — LLM for task add / task ship`);
128
- log.info(" 4. Push branches to origin (first time only):");
129
- log.info(` ${dim(`git push -u ${git.remote} ${git.productionBranch}`)}`);
130
- if (git.integrationBranch !== git.productionBranch) {
131
- log.info(` ${dim(`git push -u ${git.remote} ${git.integrationBranch}`)}`);
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 && options.allowNoRemote !== true && !hadRemoteBefore) {
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 (options.allowNoRemote === true && remote === null) {
275
- log.warn("No git remote configured (--allow-no-remote). task ship push/MR will fail until origin exists.");
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) {
@@ -1,5 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
3
+ import { diagnoseIntegrationSync, printIntegrationSyncDiagnosis, } from "../lib/git-integration-sync.js";
3
4
  import { gitFetchRemote, gitPullFastForwardOnly, gitRemoteBranchExists, gitRemoteUrl, gitSwitchToBranch, readGitInfo, } from "../lib/git.js";
4
5
  import { bold, cyan, dim, log } from "../lib/logger.js";
5
6
  /**
@@ -73,7 +74,13 @@ export async function pullCommand(options = {}) {
73
74
  }
74
75
  catch (e) {
75
76
  const msg = e instanceof Error ? e.message : String(e);
76
- log.error(`Could not fast-forward ${integrationBranch}: ${msg}. Commit or stash local changes, then rerun \`vibeops pull\`.`);
77
+ const diagnosis = await diagnoseIntegrationSync(cwd, remote, integrationBranch);
78
+ if (!diagnosis.ok) {
79
+ printIntegrationSyncDiagnosis(diagnosis);
80
+ }
81
+ else {
82
+ log.error(`Could not fast-forward ${integrationBranch}: ${msg}. Commit or stash local changes, then rerun \`vibeops pull\`.`);
83
+ }
77
84
  process.exitCode = 1;
78
85
  }
79
86
  }
@@ -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 printHuman(report) {
101
- log.blank();
102
- log.info(bold("─".repeat(56)));
103
- log.info(bold(" VibeOps · status"));
104
- log.info(bold("─".repeat(56)));
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(` ${dim("Project")} ${yellow("not initialized")} — run vibeops init`);
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(` ${dim("Focus")} ${cyan(f.id)} — ${f.title || dim("(no title)")}`);
131
- log.info(` ${dim("Status")} ${f.status}`);
132
- log.info(` ${dim("File")} ${f.file}`);
133
- if (f.goalExcerpt)
134
- log.info(` ${dim("Goal")} ${f.goalExcerpt}`);
135
- log.info(` ${dim("Result")} ${f.resultFilled ? green("filled") : yellow("empty")} · Test Result ${f.testResultFilled ? green("filled") : yellow("empty")}`);
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(` ${dim("Focus")} ${dim("(none)")}`);
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(" Git"));
142
- if (!report.git.isRepo) {
143
- log.info(` ${yellow("not a git repository")}`);
155
+ log.info(bold(" NEXT"));
156
+ for (const line of report.nextLines) {
157
+ log.info(` ${cyan("→")} ${line}`);
144
158
  }
145
- else {
146
- log.info(` ${dim("HEAD")} ${report.git.branch ?? "(detached)"}`);
147
- if (report.git.dirty === true)
148
- log.info(` ${dim("Tree")} ${yellow("dirty")}`);
149
- else if (report.git.dirty === false)
150
- log.info(` ${dim("Tree")} ${green("clean")}`);
151
- if (report.git.policy) {
152
- log.info(` ${dim("Policy")} ${report.git.policy}`);
153
- }
154
- if (report.focus?.taskBranch) {
155
- const on = report.focus.onTaskBranch === true
156
- ? green("yes")
157
- : report.focus.onTaskBranch === false
158
- ? yellow("no")
159
- : dim("—");
160
- log.info(` ${dim("Task br.")} ${report.focus.taskBranch} · on branch: ${on}`);
161
- }
162
- if (report.focus?.mergeRequestUrl) {
163
- log.info(` ${dim("MR/PR")} ${report.focus.mergeRequestUrl}`);
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
- ? green(`${report.llm.active} (ok)`)
188
+ ? `${report.llm.active} (ok)`
172
189
  : report.llm.anyAvailable
173
- ? yellow("(preference not available)")
174
- : yellow("none");
175
- log.info(` ${dim("Active")} ${active}`);
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(`${dim("Next")} ${report.nextHint}`);
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 = {}) {