@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/dist/lib/task.js CHANGED
@@ -3,33 +3,31 @@ import { basename, extname, join } from "node:path";
3
3
  import matter from "gray-matter";
4
4
  import { isDirectory, readText, writeText } from "./filesystem.js";
5
5
  import { readGitInfo } from "./git.js";
6
- const KNOWN_STATUSES = new Set([
7
- "planned",
8
- "in_progress",
6
+ const SHIPPED_ALIASES = new Set([
9
7
  "review",
10
- "blocked",
8
+ "merged",
11
9
  "done",
10
+ "shipped",
11
+ "ready_for_review",
12
12
  ]);
13
13
  export function normalizeStatus(value) {
14
14
  if (typeof value === "string") {
15
15
  const s = value.toLowerCase().replace(/\s+/g, "_");
16
- if (KNOWN_STATUSES.has(s))
17
- return s;
16
+ if (s === "in_progress" || s === "inprogress")
17
+ return "in_progress";
18
+ if (SHIPPED_ALIASES.has(s))
19
+ return "shipped";
20
+ if (s === "planned" || s === "blocked")
21
+ return "in_progress";
18
22
  }
19
- return "planned";
23
+ return "in_progress";
20
24
  }
21
25
  export function statusDisplay(status) {
22
26
  switch (status) {
23
- case "planned":
24
- return "Planned";
25
27
  case "in_progress":
26
28
  return "In Progress";
27
- case "review":
28
- return "Review";
29
- case "blocked":
30
- return "Blocked";
31
- case "done":
32
- return "Done";
29
+ case "shipped":
30
+ return "Shipped";
33
31
  }
34
32
  }
35
33
  function extractIdFromFilename(file) {
@@ -89,7 +87,7 @@ export async function readTaskFile(filePath) {
89
87
  const title = titleFromFm ?? extractTitleFromBody(body);
90
88
  const status = data["status"] !== undefined
91
89
  ? normalizeStatus(data["status"])
92
- : (extractInlineStatus(body) ?? "planned");
90
+ : (extractInlineStatus(body) ?? "in_progress");
93
91
  const mvpPhaseFromFm = typeof data["mvpPhase"] === "string" ? data["mvpPhase"] : undefined;
94
92
  const mvpPhase = mvpPhaseFromFm ?? extractInlineMvpPhase(body);
95
93
  const priority = typeof data["priority"] === "string" ? data["priority"] : undefined;
@@ -117,11 +115,8 @@ export async function scanTasks(tasksDir) {
117
115
  export function countTasks(tasks) {
118
116
  const counts = {
119
117
  total: tasks.length,
120
- planned: 0,
121
118
  in_progress: 0,
122
- review: 0,
123
- blocked: 0,
124
- done: 0,
119
+ shipped: 0,
125
120
  };
126
121
  for (const t of tasks)
127
122
  counts[t.status]++;
@@ -131,14 +126,7 @@ export function pickInProgressTask(tasks) {
131
126
  return tasks.find((t) => t.status === "in_progress") ?? null;
132
127
  }
133
128
  export function pickNextTask(tasks) {
134
- const inProgress = pickInProgressTask(tasks);
135
- if (inProgress)
136
- return inProgress;
137
- const review = tasks.find((t) => t.status === "review");
138
- if (review)
139
- return review;
140
- const planned = tasks.filter((t) => t.status === "planned");
141
- return planned[0] ?? null;
129
+ return pickInProgressTask(tasks);
142
130
  }
143
131
  const TEMPLATE_TASK_ID = "TASK-000";
144
132
  export function filterActionableTasks(tasks) {
@@ -148,19 +136,12 @@ function taskSortKey(id) {
148
136
  const m = /^TASK-(\d+)$/i.exec(id.trim());
149
137
  return m ? Number.parseInt(m[1], 10) : 0;
150
138
  }
151
- /** Latest finished TASK by numeric id (TASK-mvp sorts after numbered tasks). */
152
- export function pickLatestDoneTask(tasks) {
153
- const done = filterActionableTasks(tasks).filter((t) => t.status === "done");
154
- if (done.length === 0)
139
+ /** Latest **Shipped** TASK (for merge / sync targeting). */
140
+ export function pickLatestShippedTask(tasks) {
141
+ const shipped = filterActionableTasks(tasks).filter((t) => t.status === "shipped");
142
+ if (shipped.length === 0)
155
143
  return null;
156
- return [...done].sort((a, b) => taskSortKey(b.id) - taskSortKey(a.id))[0];
157
- }
158
- /** Latest TASK in Review (awaiting merge / sync). */
159
- export function pickLatestReviewTask(tasks) {
160
- const review = filterActionableTasks(tasks).filter((t) => t.status === "review");
161
- if (review.length === 0)
162
- return null;
163
- return [...review].sort((a, b) => taskSortKey(b.id) - taskSortKey(a.id))[0];
144
+ return [...shipped].sort((a, b) => taskSortKey(b.id) - taskSortKey(a.id))[0];
164
145
  }
165
146
  export async function loadActionableTasks(tasksDir) {
166
147
  return filterActionableTasks(await scanTasks(tasksDir));
@@ -185,7 +166,7 @@ export async function pickActiveTask(tasksDir, cwd) {
185
166
  return t;
186
167
  }
187
168
  }
188
- return pickNextTask(actionable);
169
+ return pickNextTask(actionable.filter((t) => t.status === "in_progress"));
189
170
  }
190
171
  export async function findTaskFile(tasksDir, taskId) {
191
172
  const all = await scanTasks(tasksDir);
@@ -350,6 +331,29 @@ export async function updateTaskSection(filePath, title, content) {
350
331
  await writeText(filePath, updated);
351
332
  }
352
333
  }
334
+ function parsePreviousMergeRequestUrls(content) {
335
+ const lines = content.split("\n");
336
+ const urls = [];
337
+ let inBlock = false;
338
+ for (const line of lines) {
339
+ const t = line.trim();
340
+ if (t.startsWith("- Previous Merge Requests:")) {
341
+ inBlock = true;
342
+ continue;
343
+ }
344
+ if (inBlock) {
345
+ const m = /^\s*-\s+(https?:\/\/.+)$/.exec(line);
346
+ if (m) {
347
+ urls.push(m[1].trim());
348
+ continue;
349
+ }
350
+ if (t.startsWith("- ") && !line.startsWith(" ")) {
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ return urls;
356
+ }
353
357
  function renderGitContextBlock(ctx) {
354
358
  const lines = [
355
359
  `- Base Branch: \`${ctx.baseBranch}\``,
@@ -363,11 +367,39 @@ function renderGitContextBlock(ctx) {
363
367
  if (typeof ctx.mergeRequestUrl === "string" && ctx.mergeRequestUrl.length > 0) {
364
368
  lines.push(`- Merge Request: ${ctx.mergeRequestUrl}`);
365
369
  }
370
+ const prev = ctx.previousMergeRequestUrls ?? [];
371
+ if (prev.length > 0) {
372
+ lines.push("- Previous Merge Requests:");
373
+ for (const url of prev) {
374
+ lines.push(` - ${url}`);
375
+ }
376
+ }
366
377
  if (typeof ctx.pushedAt === "string" && ctx.pushedAt.length > 0) {
367
378
  lines.push(`- Pushed At: \`${ctx.pushedAt}\``);
368
379
  }
380
+ if (typeof ctx.lastReshipAt === "string" && ctx.lastReshipAt.length > 0) {
381
+ lines.push(`- Last Reship At: \`${ctx.lastReshipAt}\``);
382
+ }
383
+ if (typeof ctx.reshipCount === "number" && ctx.reshipCount > 0) {
384
+ lines.push(`- Reship Count: \`${ctx.reshipCount}\``);
385
+ }
369
386
  return lines.join("\n");
370
387
  }
388
+ /** Archive current MR URL and bump reship metadata before opening a follow-up PR. */
389
+ export function archiveMergeRequestForReship(ctx) {
390
+ const previous = [...(ctx.previousMergeRequestUrls ?? [])];
391
+ const current = ctx.mergeRequestUrl?.trim();
392
+ if (typeof current === "string" && current.length > 0) {
393
+ previous.push(current);
394
+ }
395
+ return {
396
+ ...ctx,
397
+ previousMergeRequestUrls: previous.length > 0 ? previous : undefined,
398
+ mergeRequestUrl: undefined,
399
+ reshipCount: (ctx.reshipCount ?? 0) + 1,
400
+ lastReshipAt: new Date().toISOString(),
401
+ };
402
+ }
371
403
  export async function upsertGitContext(filePath, ctx) {
372
404
  const raw = await readText(filePath);
373
405
  const block = renderGitContextBlock(ctx);
@@ -396,6 +428,8 @@ const GIT_CTX_RE = {
396
428
  doneAt: /^-\s+Done At:\s*`([^`]+)`/m,
397
429
  mergeRequestUrl: /^-\s+Merge Request:\s*(.+)$/m,
398
430
  pushedAt: /^-\s+Pushed At:\s*`([^`]+)`/m,
431
+ lastReshipAt: /^-\s+Last Reship At:\s*`([^`]+)`/m,
432
+ reshipCount: /^-\s+Reship Count:\s*`(\d+)`/m,
399
433
  };
400
434
  export async function markGitContextDone(filePath) {
401
435
  const ctx = await readGitContext(filePath);
@@ -417,13 +451,27 @@ export async function readGitContext(filePath) {
417
451
  const doneAt = content.match(GIT_CTX_RE.doneAt)?.[1];
418
452
  const mergeRequestUrl = content.match(GIT_CTX_RE.mergeRequestUrl)?.[1]?.trim();
419
453
  const pushedAt = content.match(GIT_CTX_RE.pushedAt)?.[1];
454
+ const lastReshipAt = content.match(GIT_CTX_RE.lastReshipAt)?.[1];
455
+ const reshipCountRaw = content.match(GIT_CTX_RE.reshipCount)?.[1];
456
+ const previousMergeRequestUrls = parsePreviousMergeRequestUrls(content);
420
457
  const ctx = { baseBranch, baseCommit, taskBranch, startedAt };
421
458
  if (typeof doneAt === "string" && doneAt.length > 0)
422
459
  ctx.doneAt = doneAt;
423
460
  if (typeof mergeRequestUrl === "string" && mergeRequestUrl.length > 0) {
424
461
  ctx.mergeRequestUrl = mergeRequestUrl;
425
462
  }
463
+ if (previousMergeRequestUrls.length > 0) {
464
+ ctx.previousMergeRequestUrls = previousMergeRequestUrls;
465
+ }
426
466
  if (typeof pushedAt === "string" && pushedAt.length > 0)
427
467
  ctx.pushedAt = pushedAt;
468
+ if (typeof lastReshipAt === "string" && lastReshipAt.length > 0) {
469
+ ctx.lastReshipAt = lastReshipAt;
470
+ }
471
+ if (typeof reshipCountRaw === "string") {
472
+ const n = Number.parseInt(reshipCountRaw, 10);
473
+ if (Number.isFinite(n) && n > 0)
474
+ ctx.reshipCount = n;
475
+ }
428
476
  return ctx;
429
477
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodtek/vibeops",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Minimal CLI rails for Cursor-based TASK-driven vibe coding.",
5
5
  "type": "module",
6
6
  "author": "VibeOps contributors",
@@ -2,5 +2,5 @@
2
2
 
3
3
  - Work from `docs/tasks/TASK-NNN-*.md` only.
4
4
  - One TASK at a time.
5
- - Fill **Result** and **Test Result** before telling the human to run `vibeops task ship TASK-NNN`.
6
- - Do not merge to integration or main unless the human runs `vibeops task merge` / `task release`.
5
+ - Fill **Result** and **Test Result** before telling the human to run `vibeops task ship TASK-NNN` (or `task reship` for Shipped follow-up).
6
+ - Do not merge to integration or main unless the human runs `vibeops task merge` / `task sync` / `task release`.
@@ -8,4 +8,4 @@ alwaysApply: true
8
8
  - Use branch pattern `task/*` from `vibeops task add`.
9
9
  - Include TASK id in commits: `feat(task-nnn): …`.
10
10
  - No force-push to shared branches.
11
- - Run `vibeops task ship`, `task merge`, `task sync` when the human asks — do not merge locally in the IDE unless requested.
11
+ - Run `vibeops task ship`, `task reship`, `task merge`, `task sync` when the human asks — do not merge locally in the IDE unless requested.
@@ -5,7 +5,7 @@ alwaysApply: true
5
5
 
6
6
  # Docs before task ship
7
7
 
8
- Before `vibeops task ship`:
8
+ Before `vibeops task ship` or `vibeops task reship`:
9
9
 
10
10
  1. Fill **Result** and **Test Result** in the TASK file (paths, commands, outcomes — facts only).
11
11
  2. If architecture or decisions changed, note them in the TASK; `task ship` can update `docs/project/*` via LLM.
@@ -9,18 +9,33 @@ vibeops task add
9
9
  # Plan / build in your agent (@docs/tasks/TASK-NNN-*.md)
10
10
 
11
11
  vibeops task ship
12
+ vibeops task merge # CLI or host UI
13
+ vibeops task sync # optional — branch cleanup only
14
+
15
+ # Same TASK follow-up (after merge):
16
+ vibeops task reship TASK-NNN
12
17
  vibeops task merge
13
- vibeops task sync
14
18
  ```
15
19
 
16
20
  Optional release to production: `vibeops task release`.
17
21
 
22
+ ## TASK status (markdown)
23
+
24
+ Only two values in `## Status`:
25
+
26
+ | Status | Meaning |
27
+ |--------|---------|
28
+ | **In Progress** | Active slice (`task add`) |
29
+ | **Shipped** | Submitted (`task ship` or `task reship`) |
30
+
31
+ Merge and sync do **not** change the TASK file.
32
+
18
33
  ```bash
19
- vibeops llm connect # optional — task add / task ship
34
+ vibeops llm connect # optional — task add / ship / reship
20
35
  ```
21
36
 
22
37
  ## Rules
23
38
 
24
- - One TASK at a time; scope from `docs/tasks/TASK-NNN-*.md`.
25
- - Fill **Result** and **Test Result** before `vibeops task ship`.
26
- - Agents do not merge or sync unless the human asks.
39
+ - One **In Progress** TASK at a time; **Shipped** does not block `task add`.
40
+ - Fill **Result** and **Test Result** before `vibeops task ship` (or before `task reship` for follow-ups).
41
+ - Agents do not run `task merge`, `task sync`, or `task reship` unless the human asks.
@@ -1,6 +1,6 @@
1
1
  # Current state
2
2
 
3
- > Facts only. Updated by `vibeops task ship` / humans; stage changes on `task sync`.
3
+ > Facts only. Updated by `vibeops task ship` / humans after merge (not by `task sync`).
4
4
 
5
5
  ## Stage
6
6
 
@@ -5,5 +5,5 @@
5
5
  ## D-001 · VibeOps workflow
6
6
 
7
7
  - **Date:** {{CREATED_AT}}
8
- - **Decision:** TASK-driven development with `vibeops task add` / `task ship` / `merge` / `sync` and Cursor for plan/build.
8
+ - **Decision:** TASK-driven development with `vibeops task add` / `task ship` / `task reship` / `merge` / `sync` and Cursor for plan/build. TASK status: **In Progress** → **Shipped** only.
9
9
  - **Why:** Git `docs/tasks` beats chat as source of truth.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Planned
5
+ In Progress
6
6
 
7
7
  ## Goal
8
8
 
@@ -8,6 +8,6 @@ description: Implement the current VibeOps TASK file in the repo.
8
8
  1. Read the full `docs/tasks/TASK-NNN-*.md` (Scope, Acceptance Criteria, Test Plan).
9
9
  2. Search the codebase before adding new patterns.
10
10
  3. Implement only within the TASK scope.
11
- 4. Do not run `vibeops task ship`, `merge`, or `sync` — the human runs the CLI lifecycle.
11
+ 4. Do not run `vibeops task ship`, `reship`, `merge`, or `sync` — the human runs the CLI lifecycle.
12
12
 
13
- When implementation is ready, tell the human to run `vibeops task ship TASK-NNN`.
13
+ When implementation is ready, tell the human to run `vibeops task ship TASK-NNN`. For a **Shipped** follow-up after merge, use `vibeops task reship TASK-NNN`.