@goodtek/vibeops 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/README.md +13 -7
- package/dist/cli.js +28 -4
- package/dist/commands/status.js +10 -2
- package/dist/commands/task-add.js +1 -1
- package/dist/commands/task-reship.js +192 -0
- package/dist/commands/task-ship.js +10 -10
- package/dist/commands/task-sync.js +16 -31
- package/dist/lib/git.js +4 -0
- package/dist/lib/resolve-task.js +1 -1
- package/dist/lib/task-add-llm.js +1 -1
- package/dist/lib/task-context.js +31 -15
- package/dist/lib/task-effective-status.js +62 -0
- package/dist/lib/task-lifecycle-target.js +6 -6
- package/dist/lib/task-push-pr.js +2 -1
- package/dist/lib/task-reship.js +192 -0
- package/dist/lib/task-scaffold.js +1 -1
- package/dist/lib/task-ship-llm.js +84 -3
- package/dist/lib/task-start.js +1 -1
- package/dist/lib/task.js +89 -41
- package/package.json +1 -1
- package/templates/clients/claude/CLAUDE.md +2 -2
- package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +1 -1
- package/templates/clients/cursor/.cursor/rules/03-docs-before-ship.mdc +1 -1
- package/templates/core/AGENTS.md +20 -5
- package/templates/core/docs/project/05-current-state.md +1 -1
- package/templates/core/docs/project/06-decisions.md +1 -1
- package/templates/core/docs/tasks/TASK-000-template.md +1 -1
- package/templates/skills-shared/implement-task/SKILL.md +2 -2
package/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
|
|
7
|
-
"planned",
|
|
8
|
-
"in_progress",
|
|
6
|
+
const SHIPPED_ALIASES = new Set([
|
|
9
7
|
"review",
|
|
10
|
-
"
|
|
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 (
|
|
17
|
-
return
|
|
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 "
|
|
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 "
|
|
28
|
-
return "
|
|
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) ?? "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
export function
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
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 [...
|
|
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
|
@@ -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.
|
package/templates/core/AGENTS.md
CHANGED
|
@@ -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 /
|
|
34
|
+
vibeops llm connect # optional — task add / ship / reship
|
|
20
35
|
```
|
|
21
36
|
|
|
22
37
|
## Rules
|
|
23
38
|
|
|
24
|
-
- One TASK at a time;
|
|
25
|
-
- Fill **Result** and **Test Result** before `vibeops task ship
|
|
26
|
-
- Agents do not merge
|
|
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.
|
|
@@ -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.
|
|
@@ -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`.
|