@goodtek/vibeops 3.1.7 → 3.1.8

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,13 @@ All notable changes to VibeOps are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 3.1.8 - 2026-10-17
8
+
9
+ ### Changed
10
+
11
+ - **Time values follow official documentation** (D-009, TASK-033). `merge.waitTimeoutSeconds` default 900 → 21600 (GitHub-hosted job limit; max 432000, the self-hosted limit) — the wait still ends as soon as checks finish. `merge.emptyRollupGraceSeconds` default 30 → 0: name CI outside GitHub Actions in `requiredChecks`. `task merge` / `task release` confirm the merge with one read instead of polling (the merge APIs answer synchronously). `scripts/npm-publish.sh` waits for the registry with back-off and jitter within a few-minute budget.
12
+ - **`vibeops init` makes the integration branch the default branch** of a repository it creates on GitHub (`gh repo edit --default-branch`) or GitLab (project API `default_branch`), so PRs and clones start from `develop`. Existing repositories are not changed; on failure the manual command is printed (TASK-035).
13
+
7
14
  ## 3.1.7 - 2026-10-17
8
15
 
9
16
  ### Fixed
package/README.md CHANGED
@@ -141,6 +141,7 @@ vibeops llm use auto # auto | codex-oauth | cursor-agent | openai
141
141
  ## Flags (common)
142
142
 
143
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`
144
+ When `init` creates the repository on GitHub / GitLab (interactive), it pushes `main` and the integration branch and makes the integration branch (`develop` with the GitFlow preset) the repository's default branch; an existing repository's default branch is not changed.
144
145
  - **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
145
146
  - **`task del`**: `--dry-run`, `--force`, `--no-remote-delete`, `--no-close-mr`
146
147
  - **`task ship`**: `-m/--message`, `--new-cycle`, `--no-commit`, `--dry-run`, `--no-pr`, `--non-interactive`, `--allow-open-mr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`
@@ -220,8 +221,9 @@ Before calling `gh pr merge` / `glab mr merge`:
220
221
  - **GitLab:** the MR head pipeline must be `success` or `skipped`. `failed`, `canceled`, `manual` and unknown statuses refuse; `created` / `pending` / `running` / `scheduled` / … wait.
221
222
  - **Host output is verified:** if `gh pr view` lacks or has empty `state`, `mergeable`, `headRefOid`, or `statusCheckRollup` is not a list (GitLab: `state`, `sha`, `merge_status`, `detailed_merge_status`, `head_pipeline`), the merge is refused.
222
223
  - **Failed check → refuse immediately** (exit code 1, checks named). **Pending → wait** up to `waitTimeoutSeconds`, then refuse. There is no override flag: fix CI (re-run `task ship`) and rerun `task merge`.
223
- - **No checks at all → refuse** unless `allowNoChecks: true`. "No checks" means: empty rollup, no GitHub Actions run and no expected workflow for the head commit (GitLab: no pipeline). For CI outside GitHub Actions (commit statuses / check apps), the empty list is re-polled for `emptyRollupGraceSeconds` first; list such checks in `requiredChecks` so the gate waits for them by name.
224
- - **Waiting** polls the host (`gh pr view` + the Actions REST API) every `pollIntervalSeconds` (default 10 s, the default of `gh pr checks --watch`) until the state changes, bounded by `waitTimeoutSeconds` (default 900 s). Host read errors back off exponentially with jitter (capped at 60 s, GitHub's guidance for rate-limited clients).
224
+ - **No checks at all → refuse** unless `allowNoChecks: true`. "No checks" means: empty rollup, no GitHub Actions run and no expected workflow for the head commit (GitLab: no pipeline). For CI outside GitHub Actions (commit statuses / check apps), list such checks in `requiredChecks` so the gate waits for them by name (`emptyRollupGraceSeconds`, default 0, can re-poll an empty list first).
225
+ - **Waiting** polls the host (`gh pr view` + the Actions REST API) every `pollIntervalSeconds` (default 10 s, the default of `gh pr checks --watch`) until the state changes, bounded by `waitTimeoutSeconds` (default 21600 s, the GitHub-hosted job limit). Host read errors back off exponentially with jitter (capped at 60 s, GitHub's guidance for rate-limited clients).
226
+ - **After the merge:** `gh pr merge` / `glab mr merge` return the result synchronously (GitHub `merged: true`, GitLab `state: "merged"`), so VibeOps reads the PR/MR once to confirm. Not merged at that point (merge queue / auto-merge) is reported, not waited for.
225
227
  - **The merge is pinned to the checked commit:** right before merging the gate re-reads the PR head; if it moved, the new head is evaluated from scratch. The merge itself uses `gh pr merge --match-head-commit <sha>` (requires **gh ≥ 2.13.0**) / `glab mr merge --auto-merge=false --sha <sha>` (verified with glab 1.106), so a push between the last check and the merge makes the host refuse. A GitLab `405` re-runs the gate and retries once with the same flags.
226
228
  - **Unreadable Git Context stops `task ship` / `task add`:** a TASK file whose `## Git Context` cannot be read (e.g. an edit left `## ## Git Context`) makes `task ship` exit 1 before committing or pushing, naming the line; `task add` refuses to "resume" such a TASK (it would write a second section) and names the file and line.
227
229
  - **`task merge` refuses a stale base:** the head commit the gate approved must contain the current `<remote>/<base>` (`git merge-base --is-ancestor`, after fetching the base branch and the head commit). A head that does not — the base moved since the branch was pushed, or the branch was rebuilt on an old base (e.g. `git reset --soft <stale origin/develop>` + commit) — can silently revert work merged into the base since, and a squash merge lands that revert. Bring the branch up to date (`git fetch` + `git merge <remote>/<base>`, test, `task ship`), wait for CI, then rerun `task merge`. A fetch failure refuses (fail closed). `task release` does not apply this check (with merge-commit releases the production branch is normally not an ancestor of the integration branch).
@@ -233,9 +235,9 @@ Before calling `gh pr merge` / `glab mr merge`:
233
235
  "merge": {
234
236
  "requiredChecks": ["build", "lint*"],
235
237
  "releaseRequiredChecks": ["release smoke"],
236
- "waitTimeoutSeconds": 900,
238
+ "waitTimeoutSeconds": 21600,
237
239
  "pollIntervalSeconds": 10,
238
- "emptyRollupGraceSeconds": 30,
240
+ "emptyRollupGraceSeconds": 0,
239
241
  "allowNoChecks": false
240
242
  }
241
243
  }
@@ -245,9 +247,9 @@ Before calling `gh pr merge` / `glab mr merge`:
245
247
  |-----|---------|---------|---------|
246
248
  | `requiredChecks` | `[]` | array of non-empty strings | Checks that must be present and green for `task merge`. A missing one refuses: `required check "X" never ran`. |
247
249
  | `releaseRequiredChecks` | `[]` | array of non-empty strings | Same for `task release` (integration → production). Shown by `task release --dry-run`. |
248
- | `waitTimeoutSeconds` | `900` | integer 1–7200 | Max wait for pending checks. |
249
- | `pollIntervalSeconds` | `10` | integer 1–300, ≤ timeout | Poll interval while waiting (same default as `gh pr checks --watch`). |
250
- | `emptyRollupGraceSeconds` | `30` | integer 0–600 | Only when no GitHub Actions workflow is expected: how long an empty check list is re-polled before "no checks" applies (time for external CI to report). |
250
+ | `waitTimeoutSeconds` | `21600` | integer 1–432000 | Upper bound for pending checks. The wait ends as soon as every check finished; the bound only stops a check that never does. Default = the 6-hour job limit of GitHub-hosted runners; self-hosted runners allow up to 5 days ([GitHub Actions limits](https://docs.github.com/actions/reference/limits)). |
251
+ | `pollIntervalSeconds` | `10` | integer ≥ 1, ≤ timeout | Poll interval while waiting (same default as `gh pr checks --watch`). |
252
+ | `emptyRollupGraceSeconds` | `0` | integer ≥ 0, ≤ timeout | Only when no GitHub Actions workflow is expected: how long an empty check list is re-polled before "no checks" applies. Default `0`: name CI outside GitHub Actions in `requiredChecks` so the gate waits for it by name, instead of guessing how long it takes to report. |
251
253
  | `allowNoChecks` | `false` | boolean | Merge a PR that has no checks at all (repo without CI). Prints one warning per such merge. |
252
254
 
253
255
  - Check names are exact (`CheckRun.name` / `StatusContext.context`) or a `*` glob (`"lint*"`, `"* · linux"`). Required checks are GitHub-only; GitLab gates on the head pipeline.
@@ -3,7 +3,7 @@ import { install, printReport } from "../bootstrap/installer.js";
3
3
  import { buildConfig, isVibeopsProject, readConfig, writeConfig } from "../lib/config.js";
4
4
  import { gitAddAll, gitBranchExists, gitCheckout, gitCommit, gitCreateBranch, gitInit, gitPullFastForwardOnly, gitPush, gitRemoteBranchExists, gitRemoteUrl, gitSetDefaultBranch, gitStatusPorcelain, hasAnyCommit, isGitRepository, } from "../lib/git.js";
5
5
  import { formatGitPolicySummary, askGitPolicy, parseGitPolicyArg, resolvePreset } from "../lib/git-policy.js";
6
- import { ensureOriginRemote } from "../lib/git-remote.js";
6
+ import { ensureOriginRemote, setRemoteDefaultBranch } from "../lib/git-remote.js";
7
7
  import { askInitClients, formatClientsList, parseClientsArg, } from "../lib/init-clients.js";
8
8
  import { askInput, askYesNo } from "../lib/inquirer-helpers.js";
9
9
  import { cyan, dim, green, log, yellow } from "../lib/logger.js";
@@ -272,6 +272,19 @@ export async function initCommand(options = {}) {
272
272
  await gitCheckout(projectRoot, gitConfig.integrationBranch);
273
273
  await gitPush(projectRoot, remoteName, gitConfig.integrationBranch, true);
274
274
  log.ok(`pushed ${gitConfig.integrationBranch} → ${remoteName}`);
275
+ // A repository init just created defaults to the integration branch, so PRs and
276
+ // clones start from it; an existing repository's setting is left to its owner.
277
+ if (remote.created === true) {
278
+ if (await setRemoteDefaultBranch(projectRoot, gitConfig.host, gitConfig.integrationBranch)) {
279
+ log.ok(`default branch → ${gitConfig.integrationBranch}`);
280
+ }
281
+ else {
282
+ log.warn(`Could not set the default branch to ${gitConfig.integrationBranch}.`);
283
+ log.info(dim(gitConfig.host === "gitlab"
284
+ ? ` glab api --method PUT projects/:id --raw-field default_branch=${gitConfig.integrationBranch}`
285
+ : ` gh repo edit --default-branch ${gitConfig.integrationBranch}`));
286
+ }
287
+ }
275
288
  }
276
289
  pushedBaseline = true;
277
290
  }
@@ -145,10 +145,33 @@ async function createOnHost(cwd, remote, host, defaultBranch) {
145
145
  await runGit(cwd, ["remote", "rename", "origin", remote]);
146
146
  }
147
147
  }
148
- return { remote, url, host };
148
+ return { remote, url, host, created: true };
149
149
  }
150
150
  const url = await createGithubRepo(cwd, name);
151
- return { remote, url, host };
151
+ return { remote, url, host, created: true };
152
+ }
153
+ /**
154
+ * Make `branch` the host's default branch (official CLIs: `gh repo edit --default-branch`,
155
+ * cli.github.com/manual/gh_repo_edit; GitLab project API `default_branch` via `glab api`,
156
+ * docs.gitlab.com/api/projects/#edit-a-project; `:id` placeholder: docs.gitlab.com/cli/api/).
157
+ * Returns false (caller prints the manual step) when the CLI call fails.
158
+ */
159
+ export async function setRemoteDefaultBranch(cwd, host, branch) {
160
+ try {
161
+ if (host === "gitlab") {
162
+ await execFileAsync("glab", ["api", "--method", "PUT", "projects/:id", "--raw-field", `default_branch=${branch}`], { cwd, maxBuffer: 1024 * 1024 });
163
+ }
164
+ else {
165
+ await execFileAsync("gh", ["repo", "edit", "--default-branch", branch], {
166
+ cwd,
167
+ maxBuffer: 1024 * 1024,
168
+ });
169
+ }
170
+ return true;
171
+ }
172
+ catch {
173
+ return false;
174
+ }
152
175
  }
153
176
  /**
154
177
  * Ensures `origin` (or named remote) exists.
@@ -1,4 +1,10 @@
1
1
  import { gitIsAncestor, runGit } from "./git.js";
2
+ /** Fetch `<remote>/<baseBranch>` into its remote-tracking ref and return that ref. Throws on failure. */
3
+ export async function fetchBaseBranch(cwd, remote, baseBranch) {
4
+ const baseRef = `refs/remotes/${remote}/${baseBranch}`;
5
+ await runGit(cwd, ["fetch", "--no-tags", remote, `+refs/heads/${baseBranch}:${baseRef}`]);
6
+ return baseRef;
7
+ }
2
8
  /**
3
9
  * The MR/PR head must contain the current tip of its base branch.
4
10
  *
@@ -7,12 +13,6 @@ import { gitIsAncestor, runGit } from "./git.js";
7
13
  * into the base since; a squash merge then lands that revert. Fetches the base
8
14
  * branch and the head commit first; any fetch / read failure fails closed.
9
15
  */
10
- /** Fetch `<remote>/<baseBranch>` into its remote-tracking ref and return that ref. Throws on failure. */
11
- export async function fetchBaseBranch(cwd, remote, baseBranch) {
12
- const baseRef = `refs/remotes/${remote}/${baseBranch}`;
13
- await runGit(cwd, ["fetch", "--no-tags", remote, `+refs/heads/${baseBranch}:${baseRef}`]);
14
- return baseRef;
15
- }
16
16
  export async function checkBaseInHead(opts) {
17
17
  const { cwd, remote, baseBranch, headSha } = opts;
18
18
  const baseRef = `refs/remotes/${remote}/${baseBranch}`;
@@ -1,20 +1,32 @@
1
1
  import { join } from "node:path";
2
2
  import { readTextOrNull } from "./filesystem.js";
3
3
  import { VIBEOPS_CONFIG_FILE } from "./paths.js";
4
+ /**
5
+ * Job execution limits of GitHub Actions (docs.github.com/actions/reference/limits):
6
+ * "up to 6 hours" per job on GitHub-hosted runners, "up to 5 days" on self-hosted ones.
7
+ * A pending check cannot legitimately outlast its job, so these bound the wait.
8
+ */
9
+ const GITHUB_HOSTED_JOB_LIMIT_SECONDS = 6 * 60 * 60;
10
+ const SELF_HOSTED_JOB_LIMIT_SECONDS = 5 * 24 * 60 * 60;
4
11
  export const MERGE_GATE_DEFAULTS = {
5
12
  requiredChecks: [],
6
13
  releaseRequiredChecks: [],
7
- waitTimeoutSeconds: 900,
14
+ // The wait ends as soon as the host reports every check finished; this only bounds a
15
+ // check that never finishes. Self-hosted runners: raise it up to the 5-day job limit.
16
+ waitTimeoutSeconds: GITHUB_HOSTED_JOB_LIMIT_SECONDS,
8
17
  // Same default as `gh pr checks --watch --interval` (cli.github.com/manual/gh_pr_checks).
9
18
  pollIntervalSeconds: 10,
10
- emptyRollupGraceSeconds: 30,
19
+ // No grace by default: CI outside GitHub Actions is awaited by name via `requiredChecks`
20
+ // (like required status checks in branch protection), not by guessing how long it takes
21
+ // to report.
22
+ emptyRollupGraceSeconds: 0,
11
23
  allowNoChecks: false,
12
24
  };
13
- /** Inclusive bounds for the numeric settings (seconds). */
25
+ /** Inclusive bounds for the numeric settings (seconds); cross-checks below keep them ≤ the wait. */
14
26
  export const MERGE_GATE_LIMITS = {
15
- waitTimeoutSeconds: { min: 1, max: 7_200 },
16
- pollIntervalSeconds: { min: 1, max: 300 },
17
- emptyRollupGraceSeconds: { min: 0, max: 600 },
27
+ waitTimeoutSeconds: { min: 1, max: SELF_HOSTED_JOB_LIMIT_SECONDS },
28
+ pollIntervalSeconds: { min: 1, max: SELF_HOSTED_JOB_LIMIT_SECONDS },
29
+ emptyRollupGraceSeconds: { min: 0, max: SELF_HOSTED_JOB_LIMIT_SECONDS },
18
30
  };
19
31
  const KNOWN_KEYS = new Set([
20
32
  "requiredChecks",
@@ -91,6 +103,9 @@ export function parseMergeConfig(raw) {
91
103
  }
92
104
  cfg.allowNoChecks = o.allowNoChecks;
93
105
  }
106
+ if (cfg.emptyRollupGraceSeconds > cfg.waitTimeoutSeconds) {
107
+ throw new MergeConfigError("`emptyRollupGraceSeconds` must not exceed `waitTimeoutSeconds`.");
108
+ }
94
109
  if (cfg.pollIntervalSeconds > cfg.waitTimeoutSeconds) {
95
110
  throw new MergeConfigError("`pollIntervalSeconds` must not exceed `waitTimeoutSeconds`.");
96
111
  }
@@ -156,7 +156,9 @@ export async function refreshCodexOAuthTokens(refreshToken) {
156
156
  }
157
157
  /**
158
158
  * Returns a usable access token, refreshing and persisting to auth.json when
159
- * the JWT is near expiry (same pattern as Hermes `resolve_codex_runtime_credentials`).
159
+ * the JWT is near expiry. `skewSeconds` is clock-skew leeway on the `exp` claim:
160
+ * RFC 7519 §4.1.4 allows "some small leeway, usually no more than a few minutes, to
161
+ * account for clock skew" (rfc-editor.org/rfc/rfc7519#section-4.1.4); 120 s stays within it.
160
162
  */
161
163
  export async function resolveCodexOAuthAccessToken(skewSeconds = 120) {
162
164
  const loaded = await readAuthPayload();
@@ -318,20 +318,6 @@ export async function readMergeRequestDetails(cwd, host, url, options) {
318
318
  baseBranch: parsed.baseRefName ?? null,
319
319
  };
320
320
  }
321
- /** Poll host until MR/PR state is merged (or timeout). */
322
- export async function waitForMergeRequestMerged(cwd, host, url, options = {}) {
323
- const timeoutMs = options.timeoutMs ?? 120_000;
324
- const intervalMs = options.intervalMs ?? 2_000;
325
- const deadline = Date.now() + timeoutMs;
326
- while (Date.now() < deadline) {
327
- const details = await getMergeRequestDetails(cwd, host, url);
328
- if (details?.state === "merged" && details.mergedAt !== null) {
329
- return true;
330
- }
331
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
332
- }
333
- return false;
334
- }
335
321
  function mergeRequestReadinessFromDetails(details) {
336
322
  if (details === null)
337
323
  return null;
@@ -1,22 +1,21 @@
1
1
  import { mergeRequestLabel } from "./git-host.js";
2
- import { getMergeRequestDetails, waitForMergeRequestMerged, } from "./pr-create.js";
2
+ import { getMergeRequestDetails } from "./pr-create.js";
3
+ /**
4
+ * Read the MR/PR once after `gh pr merge` / `glab mr merge` returned. Both merge APIs
5
+ * answer synchronously — GitHub `200` with `merged: true`
6
+ * (docs.github.com/rest/pulls/pulls#merge-a-pull-request), GitLab the merge request with
7
+ * `state: "merged"` (docs.gitlab.com/api/merge_requests/#merge-a-merge-request; VibeOps
8
+ * merges with `--auto-merge=false`) — so there is nothing to poll for. Not merged here
9
+ * means the host queued it (merge queue / auto-merge): report, do not wait.
10
+ */
3
11
  export async function assertMergeRequestMerged(input) {
4
12
  const label = mergeRequestLabel(input.host);
5
- if (input.wait !== false) {
6
- const merged = await waitForMergeRequestMerged(input.cwd, input.host, input.url);
7
- if (!merged) {
8
- return {
9
- ok: false,
10
- message: `${label} merge did not complete (still open or auto-merge pending). Wait for Merged on the host, or rerun \`vibeops task merge\`.`,
11
- };
12
- }
13
- }
14
13
  const details = await getMergeRequestDetails(input.cwd, input.host, input.url);
15
14
  if (details?.state !== "merged" || details.mergedAt === null) {
16
15
  const state = details?.state ?? "unknown";
17
16
  return {
18
17
  ok: false,
19
- message: `${label} is ${state}, not merged into ${input.integrationBranch}. Do not run task sync until the host shows Merged.`,
18
+ message: `${label} is ${state}, not merged into ${input.integrationBranch} (merge queue or auto-merge pending?). Wait for Merged on the host before task sync, or rerun \`vibeops task merge\`.`,
20
19
  };
21
20
  }
22
21
  return { ok: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodtek/vibeops",
3
- "version": "3.1.7",
3
+ "version": "3.1.8",
4
4
  "description": "Minimal CLI rails for Cursor-based TASK-driven vibe coding.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.15.9",