@goodtek/vibeops 2.1.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 +13 -0
- package/README.md +7 -1
- package/dist/cli.js +28 -4
- package/dist/commands/status.js +1 -0
- package/dist/commands/task-reship.js +192 -0
- package/dist/lib/git.js +4 -0
- package/dist/lib/task-context.js +6 -0
- package/dist/lib/task-push-pr.js +2 -1
- package/dist/lib/task-reship.js +192 -0
- package/dist/lib/task-ship-llm.js +81 -0
- package/dist/lib/task.js +67 -0
- 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/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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to VibeOps are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.1.1 - 2026-06-08
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Docs:** living markdown, templates, and Cursor rules aligned with 2.1 lifecycle (`task reship`, two-status model, sync does not edit TASK md).
|
|
12
|
+
- **`task sync` CLI help:** description no longer says "mark Done".
|
|
13
|
+
|
|
14
|
+
## 2.1.0 - 2026-06-03
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`vibeops task reship [TASK-NNN]`** — follow-up on a **Shipped** TASK: integrate develop, archive prior MR, new MR/PR; Status stays **Shipped**.
|
|
19
|
+
|
|
7
20
|
### Changed
|
|
8
21
|
|
|
9
22
|
- **TASK md status:** only **In Progress** → **Shipped** (`task ship`). Merge (CLI/UI) and **`task sync`** do not edit the TASK file.
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on
|
|
|
14
14
|
| `vibeops init` | Core docs + agent packs (cursor / claude / codex) |
|
|
15
15
|
| `vibeops task add` | New `TASK-NNN` file + task branch |
|
|
16
16
|
| `vibeops task ship` | LLM summary, commit, push, open MR/PR (Status → **Shipped**) |
|
|
17
|
+
| `vibeops task reship` | Shipped TASK follow-up: integrate develop, new MR/PR (Status stays Shipped) |
|
|
17
18
|
| `vibeops task merge` | Merge TASK MR/PR into integration branch (default: squash) |
|
|
18
19
|
| `vibeops task sync` | After merge: ff-only integration pull, delete task branches (no TASK md edits) |
|
|
19
20
|
| `vibeops task release` | Release PR: integration → production (GitFlow) |
|
|
@@ -52,6 +53,9 @@ vibeops task add
|
|
|
52
53
|
|
|
53
54
|
vibeops task ship
|
|
54
55
|
vibeops task merge
|
|
56
|
+
# Follow-up on same TASK (after merge):
|
|
57
|
+
vibeops task reship TASK-NNN
|
|
58
|
+
vibeops task merge
|
|
55
59
|
vibeops task sync
|
|
56
60
|
vibeops task add
|
|
57
61
|
```
|
|
@@ -66,7 +70,7 @@ Only one TASK **In Progress** at a time (`task add` blocks otherwise). **Shipped
|
|
|
66
70
|
|
|
67
71
|
## LLM (optional)
|
|
68
72
|
|
|
69
|
-
For **`task add`** / **`task ship`** only (not for coding in the IDE):
|
|
73
|
+
For **`task add`** / **`task ship`** / **`task reship`** only (not for coding in the IDE):
|
|
70
74
|
|
|
71
75
|
```bash
|
|
72
76
|
vibeops llm connect
|
|
@@ -92,6 +96,7 @@ pnpm install && pnpm build && pnpm smoke
|
|
|
92
96
|
- **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--allow-no-remote`, `--cwd`
|
|
93
97
|
- **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
|
|
94
98
|
- **`task ship`**: `--dry-run`, `--no-pr`
|
|
99
|
+
- **`task reship`**: `--dry-run`, `--no-pr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`, `--allow-open-mr`, `--allow-dirty`
|
|
95
100
|
- **`task merge`**: `--dry-run`, `--merge`, `--rebase`
|
|
96
101
|
- **`task sync`**: `--dry-run`, `--no-remote-delete`, `--force`
|
|
97
102
|
- **`task release`**: `--dry-run`, `--no-merge`, `--merge`, `--rebase`
|
|
@@ -102,6 +107,7 @@ pnpm install && pnpm build && pnpm smoke
|
|
|
102
107
|
- **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
|
|
103
108
|
- **`task add`**: `task/<slug>` from **integration** (e.g. `develop`).
|
|
104
109
|
- **`task ship`**: commit → push → MR/PR → Status **Shipped** on task branch.
|
|
110
|
+
- **`task reship`**: Shipped TASK only; integrates `develop`; opens **new** PR (archives previous MR in Git Context).
|
|
105
111
|
- **`task merge`**: merge MR/PR into integration (CLI or host UI; TASK md unchanged).
|
|
106
112
|
- **`task sync`**: integration ff-only pull → delete `task/*` branches (TASK md unchanged).
|
|
107
113
|
- **`task release`**: integration → production PR + merge (skipped on trunk policy).
|
package/dist/cli.js
CHANGED
|
@@ -7,13 +7,14 @@ import { taskAddCommand } from "./commands/task-add.js";
|
|
|
7
7
|
import { taskMergeCommand } from "./commands/task-merge.js";
|
|
8
8
|
import { taskReleaseCommand } from "./commands/task-release.js";
|
|
9
9
|
import { taskShipCommand } from "./commands/task-ship.js";
|
|
10
|
+
import { taskReshipCommand } from "./commands/task-reship.js";
|
|
10
11
|
import { taskSyncCommand } from "./commands/task-sync.js";
|
|
11
12
|
import { loadVibeopsEnv } from "./lib/env.js";
|
|
12
13
|
import { VERSION } from "./version.js";
|
|
13
14
|
const program = new Command();
|
|
14
15
|
program
|
|
15
16
|
.name("vibeops")
|
|
16
|
-
.description("VibeOps — TASK workflow (init · task add/ship/merge/sync · status · llm)")
|
|
17
|
+
.description("VibeOps — TASK workflow (init · task add/ship/reship/merge/sync · status · llm)")
|
|
17
18
|
.version(VERSION, "-v, --version", "Print the VibeOps version");
|
|
18
19
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
19
20
|
const opts = actionCommand.opts();
|
|
@@ -56,7 +57,7 @@ program
|
|
|
56
57
|
gitHost: opts.gitHost,
|
|
57
58
|
});
|
|
58
59
|
});
|
|
59
|
-
const task = program.command("task").description("TASK lifecycle (add · ship · merge · sync · release)");
|
|
60
|
+
const task = program.command("task").description("TASK lifecycle (add · ship · reship · merge · sync · release)");
|
|
60
61
|
task
|
|
61
62
|
.command("add")
|
|
62
63
|
.description("Create next TASK-NNN, task branch, In Progress")
|
|
@@ -74,7 +75,7 @@ task
|
|
|
74
75
|
});
|
|
75
76
|
task
|
|
76
77
|
.command("ship [taskRef]")
|
|
77
|
-
.description("Submit TASK: LLM summary, commit, push, open MR/PR (Status →
|
|
78
|
+
.description("Submit TASK: LLM summary, commit, push, open MR/PR (Status → Shipped)")
|
|
78
79
|
.option("--dry-run", "Plan only")
|
|
79
80
|
.option("--no-pr", "Push only; skip creating MR/PR")
|
|
80
81
|
.option("--cwd <path>", "Target directory")
|
|
@@ -85,6 +86,29 @@ task
|
|
|
85
86
|
cwd: opts.cwd,
|
|
86
87
|
});
|
|
87
88
|
});
|
|
89
|
+
task
|
|
90
|
+
.command("reship [taskRef]")
|
|
91
|
+
.description("Follow-up on Shipped TASK: integrate develop, commit, new MR/PR")
|
|
92
|
+
.option("--dry-run", "Plan only")
|
|
93
|
+
.option("--no-pr", "Push only; skip creating MR/PR")
|
|
94
|
+
.option("--no-integrate", "Skip merging integration branch into task branch")
|
|
95
|
+
.option("--recreate-branch", "Recreate task branch from integration if missing")
|
|
96
|
+
.option("--skip-llm", "Do not run LLM for Result / Test Result")
|
|
97
|
+
.option("--allow-open-mr", "Allow reship while the recorded MR is still open")
|
|
98
|
+
.option("--allow-dirty", "Proceed when only governance paths are dirty")
|
|
99
|
+
.option("--cwd <path>", "Target directory")
|
|
100
|
+
.action(async (taskRef, opts) => {
|
|
101
|
+
await taskReshipCommand(taskRef, {
|
|
102
|
+
dryRun: Boolean(opts.dryRun),
|
|
103
|
+
noPr: Boolean(opts.noPr),
|
|
104
|
+
noIntegrate: Boolean(opts.noIntegrate),
|
|
105
|
+
recreateBranch: Boolean(opts.recreateBranch),
|
|
106
|
+
skipLlm: Boolean(opts.skipLlm),
|
|
107
|
+
allowOpenMr: Boolean(opts.allowOpenMr),
|
|
108
|
+
allowDirty: Boolean(opts.allowDirty),
|
|
109
|
+
cwd: opts.cwd,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
88
112
|
task
|
|
89
113
|
.command("merge [taskRef]")
|
|
90
114
|
.description("Merge TASK MR/PR into integration branch (default: squash)")
|
|
@@ -102,7 +126,7 @@ task
|
|
|
102
126
|
});
|
|
103
127
|
task
|
|
104
128
|
.command("sync [taskRef]")
|
|
105
|
-
.description("After MR merge:
|
|
129
|
+
.description("After MR merge: integration pull and delete task branch (TASK md unchanged)")
|
|
106
130
|
.option("--dry-run", "Plan only")
|
|
107
131
|
.option("--no-remote-delete", "Keep the task branch on the remote")
|
|
108
132
|
.option("--force", "Delete local task branch with -D if not fully merged")
|
package/dist/commands/status.js
CHANGED
|
@@ -70,6 +70,7 @@ async function buildReport(cwd) {
|
|
|
70
70
|
hasMergeRequest: typeof mergeRequestUrl === "string" && mergeRequestUrl.length > 0,
|
|
71
71
|
mergeRequestMerged,
|
|
72
72
|
needsSync,
|
|
73
|
+
hasLocalChanges: git.dirty === true,
|
|
73
74
|
});
|
|
74
75
|
const llm = isVibeopsProject ? await buildLlmStatusReport(cwd) : null;
|
|
75
76
|
return {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { readText } from "../lib/filesystem.js";
|
|
3
|
+
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
4
|
+
import { projectPaths } from "../lib/paths.js";
|
|
5
|
+
import { relPath } from "../lib/task-context.js";
|
|
6
|
+
import { commitDirtyWorkingTree, docsCommitMessageFor, featCommitMessageFor, pushBranch, } from "../lib/task-git-commit.js";
|
|
7
|
+
import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
|
|
8
|
+
import { applyTaskShipMemory, llmCompleteTaskReship, writeTaskResultSections, } from "../lib/task-ship-llm.js";
|
|
9
|
+
import { assertMergeRequestNotOpen, ensureTaskBranchForReship, integrateIntegrationIntoTaskBranch, loadGitConfigForReship, prepareGitContextForReship, refreshGitContextBaseCommit, resolveReshipTarget, } from "../lib/task-reship.js";
|
|
10
|
+
import { hasNonEmptySection, parseTaskFilename, readGitContext, upsertGitContext, } from "../lib/task.js";
|
|
11
|
+
import { listWorkingTreeRelPaths } from "../lib/git.js";
|
|
12
|
+
async function commitReshipMetadataAndPush(opts) {
|
|
13
|
+
if (opts.dryRun) {
|
|
14
|
+
log.info(dim(" would commit reship metadata (Status stays Shipped)"));
|
|
15
|
+
if (opts.push !== null) {
|
|
16
|
+
log.info(dim(` would git push ${opts.push.remote} ${opts.push.branch} (reship metadata)`));
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const committed = await commitDirtyWorkingTree(opts.cwd, docsCommitMessageFor(opts.taskId, "reship metadata"), false);
|
|
21
|
+
if (!committed) {
|
|
22
|
+
log.info(dim("No reship metadata commit (Git Context already on disk)."));
|
|
23
|
+
}
|
|
24
|
+
if (opts.push === null)
|
|
25
|
+
return true;
|
|
26
|
+
try {
|
|
27
|
+
await pushBranch(opts.cwd, opts.push.remote, opts.push.branch, false);
|
|
28
|
+
log.ok(`Pushed reship metadata → ${opts.push.remote}/${opts.push.branch}`);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
33
|
+
log.error(`Reship metadata push failed: ${msg}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function taskReshipCommand(taskRef, options = {}) {
|
|
38
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
39
|
+
const paths = projectPaths(cwd);
|
|
40
|
+
const dryRun = options.dryRun === true;
|
|
41
|
+
const target = await resolveReshipTarget(paths, cwd, taskRef);
|
|
42
|
+
if (target === null) {
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const { taskId, taskFile, meta, gitCtx: initialCtx } = target;
|
|
47
|
+
const relFile = relPath(cwd, taskFile);
|
|
48
|
+
log.info(bold(`vibeops task reship ${taskId}`));
|
|
49
|
+
log.info(` ${dim("file")} ${relFile}`);
|
|
50
|
+
log.info(` ${dim("branch")} ${initialCtx.taskBranch}`);
|
|
51
|
+
log.info(` ${dim("status")} Shipped (unchanged)`);
|
|
52
|
+
log.blank();
|
|
53
|
+
const gitCfg = dryRun
|
|
54
|
+
? await loadGitConfigForReship(cwd).catch(() => null)
|
|
55
|
+
: await loadGitConfigForReship(cwd);
|
|
56
|
+
if (!dryRun && gitCfg === null) {
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const integrationBranch = gitCfg?.integrationBranch ?? "develop";
|
|
61
|
+
const remote = gitCfg?.remote ?? "origin";
|
|
62
|
+
if (!dryRun && !(await assertMergeRequestNotOpen(cwd, taskFile, options.allowOpenMr === true))) {
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const branchOk = await ensureTaskBranchForReship({
|
|
67
|
+
cwd,
|
|
68
|
+
taskFile,
|
|
69
|
+
gitCtx: initialCtx,
|
|
70
|
+
integrationBranch,
|
|
71
|
+
remote,
|
|
72
|
+
recreateBranch: options.recreateBranch === true,
|
|
73
|
+
dryRun,
|
|
74
|
+
allowDirty: options.allowDirty === true,
|
|
75
|
+
});
|
|
76
|
+
if (!branchOk) {
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (options.noIntegrate !== true) {
|
|
81
|
+
const integrated = await integrateIntegrationIntoTaskBranch({
|
|
82
|
+
cwd,
|
|
83
|
+
integrationBranch,
|
|
84
|
+
remote,
|
|
85
|
+
dryRun,
|
|
86
|
+
});
|
|
87
|
+
if (!integrated) {
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
log.info(dim("Skipped integration merge (--no-integrate)."));
|
|
94
|
+
}
|
|
95
|
+
const body = await readText(taskFile);
|
|
96
|
+
let resultOk = hasNonEmptySection(body, "Result");
|
|
97
|
+
let testOk = hasNonEmptySection(body, "Test Result");
|
|
98
|
+
if (!options.skipLlm && !dryRun) {
|
|
99
|
+
log.step("LLM — follow-up Result / Test Result");
|
|
100
|
+
const patch = await llmCompleteTaskReship({
|
|
101
|
+
cwd,
|
|
102
|
+
taskId,
|
|
103
|
+
taskTitle: meta.title,
|
|
104
|
+
taskBody: body,
|
|
105
|
+
taskFileRel: relFile,
|
|
106
|
+
});
|
|
107
|
+
if (patch !== null) {
|
|
108
|
+
await writeTaskResultSections(taskFile, patch.result, patch.testResult);
|
|
109
|
+
log.ok(`Result / Test Result updated (${patch.provider})`);
|
|
110
|
+
const memory = await applyTaskShipMemory(cwd, patch);
|
|
111
|
+
for (const p of memory.updated)
|
|
112
|
+
log.ok(`Updated ${p}`);
|
|
113
|
+
for (const p of memory.skipped)
|
|
114
|
+
log.skip(`Skipped ${p}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
log.warn("LLM unavailable — ensure Result / Test Result describe this follow-up.");
|
|
118
|
+
}
|
|
119
|
+
const body2 = await readText(taskFile);
|
|
120
|
+
resultOk = hasNonEmptySection(body2, "Result");
|
|
121
|
+
testOk = hasNonEmptySection(body2, "Test Result");
|
|
122
|
+
}
|
|
123
|
+
else if (options.skipLlm) {
|
|
124
|
+
log.info(dim("LLM skipped (--skip-llm)."));
|
|
125
|
+
}
|
|
126
|
+
log.info(bold("Sections"));
|
|
127
|
+
log.info(` ${resultOk ? green("✓") : yellow("·")} Result`);
|
|
128
|
+
log.info(` ${testOk ? green("✓") : yellow("·")} Test Result`);
|
|
129
|
+
log.blank();
|
|
130
|
+
if (dryRun) {
|
|
131
|
+
log.info(bold("dry-run — would also:"));
|
|
132
|
+
log.info(" · archive prior MR URL; open new MR/PR (unless --no-pr)");
|
|
133
|
+
log.info(" · feat commit + reship metadata push");
|
|
134
|
+
log.blank();
|
|
135
|
+
log.info(`Next: ${cyan("vibeops task merge")}, then optional ${cyan("task sync")}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!resultOk || !testOk) {
|
|
139
|
+
log.error("Fill Result and Test Result for this follow-up, then rerun.");
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const dirtyBefore = await listWorkingTreeRelPaths(cwd);
|
|
144
|
+
if (dirtyBefore.length === 0) {
|
|
145
|
+
log.error("No changes to reship. Edit files on the task branch, then rerun.");
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const parts = parseTaskFilename(taskFile);
|
|
150
|
+
await commitDirtyWorkingTree(cwd, featCommitMessageFor(parts.id, meta.title), false);
|
|
151
|
+
let gitCtx = await readGitContext(taskFile);
|
|
152
|
+
if (gitCtx === null) {
|
|
153
|
+
log.error("Git Context missing after commit.");
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
gitCtx = await refreshGitContextBaseCommit(taskFile, gitCtx, cwd);
|
|
158
|
+
gitCtx = await prepareGitContextForReship(taskFile, gitCtx);
|
|
159
|
+
await upsertGitContext(taskFile, gitCtx);
|
|
160
|
+
const prResult = await finishTaskWithPullRequest({
|
|
161
|
+
cwd,
|
|
162
|
+
taskFile,
|
|
163
|
+
skipPr: options.noPr === true,
|
|
164
|
+
forceNewMergeRequest: true,
|
|
165
|
+
});
|
|
166
|
+
if (!prResult.ok) {
|
|
167
|
+
log.error("Reship failed — fix push/MR, then rerun task reship.");
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (prResult.mergeRequestUrl) {
|
|
172
|
+
log.info(` ${dim("MR/PR")} ${prResult.mergeRequestUrl}`);
|
|
173
|
+
}
|
|
174
|
+
const gitCtxAfter = await readGitContext(taskFile);
|
|
175
|
+
let pushTarget = null;
|
|
176
|
+
if (gitCtxAfter !== null && gitCfg !== null) {
|
|
177
|
+
pushTarget = { remote: gitCfg.remote, branch: gitCtxAfter.taskBranch };
|
|
178
|
+
}
|
|
179
|
+
const pushed = await commitReshipMetadataAndPush({
|
|
180
|
+
cwd,
|
|
181
|
+
taskFile,
|
|
182
|
+
taskId: parts.id,
|
|
183
|
+
dryRun: false,
|
|
184
|
+
push: pushTarget,
|
|
185
|
+
});
|
|
186
|
+
if (!pushed) {
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
log.blank();
|
|
191
|
+
log.info(`Next: ${cyan("vibeops task merge")} (or merge in the host UI), then ${cyan("vibeops task sync")}`);
|
|
192
|
+
}
|
package/dist/lib/git.js
CHANGED
|
@@ -505,6 +505,10 @@ export async function gitDeleteRemoteBranch(cwd, remote, branch) {
|
|
|
505
505
|
export async function gitPullFastForwardOnly(cwd, remote, branch) {
|
|
506
506
|
await runGit(cwd, ["pull", "--ff-only", remote, branch]);
|
|
507
507
|
}
|
|
508
|
+
/** Merge `remote/branch` into the current HEAD (follow-up integration). */
|
|
509
|
+
export async function gitMergeRemoteBranch(cwd, remote, branch) {
|
|
510
|
+
await runGit(cwd, ["merge", `${remote}/${branch}`, "--no-edit"]);
|
|
511
|
+
}
|
|
508
512
|
export async function gitPush(cwd, remote, branch, setUpstream = true) {
|
|
509
513
|
const args = setUpstream ? ["push", "-u", remote, branch] : ["push", remote, branch];
|
|
510
514
|
await runGit(cwd, args);
|
package/dist/lib/task-context.js
CHANGED
|
@@ -48,6 +48,8 @@ export function computeNextHint(input) {
|
|
|
48
48
|
return "task-sync";
|
|
49
49
|
if (input.hasMergeRequest && !input.mergeRequestMerged)
|
|
50
50
|
return "task-merge";
|
|
51
|
+
if (input.hasLocalChanges && input.onTaskBranch === true)
|
|
52
|
+
return "task-reship";
|
|
51
53
|
return "task-add";
|
|
52
54
|
}
|
|
53
55
|
if (!input.resultFilled || !input.testFilled) {
|
|
@@ -63,6 +65,10 @@ export function hintToText(hint, cwd, task) {
|
|
|
63
65
|
return "Run `vibeops task add` to start the next slice.";
|
|
64
66
|
case "task-ship":
|
|
65
67
|
return task ? `Run \`vibeops task ship ${task.id}\`.` : "Run `vibeops task ship`.";
|
|
68
|
+
case "task-reship":
|
|
69
|
+
return task
|
|
70
|
+
? `Follow-up on ${task.id}? Edit on task branch, then \`vibeops task reship ${task.id}\`.`
|
|
71
|
+
: "Run `vibeops task reship TASK-NNN` for a Shipped follow-up.";
|
|
66
72
|
case "task-merge":
|
|
67
73
|
return task
|
|
68
74
|
? `Merge the MR (UI or \`vibeops task merge ${task.id}\`), then optional task sync.`
|
package/dist/lib/task-push-pr.js
CHANGED
|
@@ -59,7 +59,8 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
59
59
|
if (git.branch !== gitCtx.taskBranch) {
|
|
60
60
|
log.warn(`Current branch is ${git.branch ?? "(detached)"}, not ${gitCtx.taskBranch}. Push may fail.`);
|
|
61
61
|
}
|
|
62
|
-
if (isExistingMergeRequestUrl(gitCtx.mergeRequestUrl)
|
|
62
|
+
if (isExistingMergeRequestUrl(gitCtx.mergeRequestUrl) &&
|
|
63
|
+
opts.forceNewMergeRequest !== true) {
|
|
63
64
|
log.info(dim(`Merge request already recorded: ${gitCtx.mergeRequestUrl}`));
|
|
64
65
|
return { ok: true, mergeRequestUrl: gitCtx.mergeRequestUrl, pushed: true };
|
|
65
66
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { GitConfigError, requireGitConfig } from "./git-config.js";
|
|
3
|
+
import { gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitFetchRemote, gitGovernanceOnlyDirty, gitHeadCommit, gitMergeRemoteBranch, gitMergeInProgress, gitPullFastForwardOnly, gitRemoteBranchExists, gitSwitchToBranch, listUnmergedRelPaths, readGitInfo, } from "./git.js";
|
|
4
|
+
import { dim, log } from "./logger.js";
|
|
5
|
+
import { getMergeRequestStateForTask } from "./task-effective-status.js";
|
|
6
|
+
import { normalizeTaskRef } from "./resolve-task.js";
|
|
7
|
+
import { archiveMergeRequestForReship, findTaskFile, loadActionableTasks, pickLatestShippedTask, readGitContext, readTaskFile, upsertGitContext, } from "./task.js";
|
|
8
|
+
export async function resolveReshipTarget(paths, cwd, taskRef) {
|
|
9
|
+
const trimmed = taskRef?.trim();
|
|
10
|
+
let taskId;
|
|
11
|
+
let taskFile;
|
|
12
|
+
if (trimmed !== undefined && trimmed.length > 0) {
|
|
13
|
+
taskId = normalizeTaskRef(trimmed);
|
|
14
|
+
taskFile = await findTaskFile(paths.docsTasks, taskId);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const shipped = pickLatestShippedTask(await loadActionableTasks(paths.docsTasks));
|
|
18
|
+
if (shipped === null) {
|
|
19
|
+
log.error(reshipNotFoundMessage(undefined));
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
taskId = shipped.id;
|
|
23
|
+
taskFile = shipped.filePath;
|
|
24
|
+
}
|
|
25
|
+
if (taskFile === null) {
|
|
26
|
+
log.error(reshipNotFoundMessage(taskRef));
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const meta = await readTaskFile(taskFile);
|
|
30
|
+
if (meta.status !== "shipped") {
|
|
31
|
+
log.error(`${taskId} is ${meta.status === "in_progress" ? "In Progress" : meta.status} — use \`vibeops task ship\` for first submit.`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const gitCtx = await readGitContext(taskFile);
|
|
35
|
+
if (gitCtx === null) {
|
|
36
|
+
log.error(`No Git Context on ${taskId}. Cannot reship.`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return { taskId, taskFile, meta, gitCtx };
|
|
40
|
+
}
|
|
41
|
+
export function reshipNotFoundMessage(taskRef) {
|
|
42
|
+
if (taskRef?.trim()) {
|
|
43
|
+
return `Shipped TASK not found: ${normalizeTaskRef(taskRef)}. Check docs/tasks/.`;
|
|
44
|
+
}
|
|
45
|
+
return "No Shipped TASK found. Pass `vibeops task reship TASK-NNN`.";
|
|
46
|
+
}
|
|
47
|
+
/** Checkout or recreate the task branch without changing TASK Status. */
|
|
48
|
+
export async function ensureTaskBranchForReship(opts) {
|
|
49
|
+
const cwd = resolve(opts.cwd);
|
|
50
|
+
const { taskBranch } = opts.gitCtx;
|
|
51
|
+
const git = await readGitInfo(cwd);
|
|
52
|
+
if (!git.isRepo) {
|
|
53
|
+
log.error("Not a git repository.");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (git.dirty === true && !opts.allowDirty) {
|
|
57
|
+
const gov = await gitGovernanceOnlyDirty(cwd);
|
|
58
|
+
if (!gov.onlyGovernance) {
|
|
59
|
+
log.error("Working tree is dirty. Commit or stash, or rerun with --allow-dirty.");
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
log.warn("Only governance paths are dirty — proceeding.");
|
|
63
|
+
}
|
|
64
|
+
const localExists = await gitBranchExists(cwd, taskBranch);
|
|
65
|
+
const remoteExists = await gitRemoteBranchExists(cwd, opts.remote, taskBranch);
|
|
66
|
+
if (opts.dryRun) {
|
|
67
|
+
if (localExists) {
|
|
68
|
+
log.info(dim(` would git switch ${taskBranch}`));
|
|
69
|
+
}
|
|
70
|
+
else if (opts.recreateBranch) {
|
|
71
|
+
log.info(dim(` would recreate ${taskBranch} from ${opts.integrationBranch}`));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
log.error(`Local branch ${taskBranch} missing. Rerun with --recreate-branch.`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (localExists) {
|
|
80
|
+
await gitCheckout(cwd, taskBranch);
|
|
81
|
+
log.ok(`On ${taskBranch}`);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (!opts.recreateBranch) {
|
|
85
|
+
if (remoteExists) {
|
|
86
|
+
log.error(`Local ${taskBranch} missing. Fetch/checkout the branch or rerun with --recreate-branch.`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
log.error(`Branch ${taskBranch} not found locally or on ${opts.remote}. Use --recreate-branch.`);
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const ok = await gitSwitchToBranch(cwd, opts.integrationBranch, opts.remote);
|
|
94
|
+
if (!ok) {
|
|
95
|
+
log.error(`Integration branch "${opts.integrationBranch}" not found.`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (await gitRemoteBranchExists(cwd, opts.remote, opts.integrationBranch)) {
|
|
99
|
+
try {
|
|
100
|
+
await gitPullFastForwardOnly(cwd, opts.remote, opts.integrationBranch);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
104
|
+
log.error(`Could not update ${opts.integrationBranch}: ${msg}`);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const head = await gitHeadCommit(cwd);
|
|
109
|
+
if (head === null || head.length === 0) {
|
|
110
|
+
log.error("Integration branch has no commits.");
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
await gitCheckoutNewBranch(cwd, taskBranch, opts.integrationBranch);
|
|
114
|
+
await upsertGitContext(opts.taskFile, {
|
|
115
|
+
...opts.gitCtx,
|
|
116
|
+
baseBranch: opts.integrationBranch,
|
|
117
|
+
baseCommit: head,
|
|
118
|
+
});
|
|
119
|
+
log.ok(`Recreated ${taskBranch} from ${opts.integrationBranch}`);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/** Merge latest integration into the current task branch. */
|
|
123
|
+
export async function integrateIntegrationIntoTaskBranch(opts) {
|
|
124
|
+
const cwd = resolve(opts.cwd);
|
|
125
|
+
if (opts.dryRun) {
|
|
126
|
+
log.info(dim(` would git fetch ${opts.remote} && git merge ${opts.remote}/${opts.integrationBranch}`));
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await gitFetchRemote(cwd, opts.remote);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
134
|
+
log.warn(`git fetch failed (${msg}). Continuing with local refs.`);
|
|
135
|
+
}
|
|
136
|
+
if (!(await gitRemoteBranchExists(cwd, opts.remote, opts.integrationBranch))) {
|
|
137
|
+
log.warn(`No ${opts.remote}/${opts.integrationBranch} — skipped integrate.`);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await gitMergeRemoteBranch(cwd, opts.remote, opts.integrationBranch);
|
|
142
|
+
log.ok(`Merged ${opts.remote}/${opts.integrationBranch} into task branch`);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
147
|
+
if (await gitMergeInProgress(cwd)) {
|
|
148
|
+
const conflicts = await listUnmergedRelPaths(cwd);
|
|
149
|
+
log.error(`Merge conflict with ${opts.integrationBranch}. Resolve, commit, then rerun.`);
|
|
150
|
+
if (conflicts.length > 0) {
|
|
151
|
+
log.info(dim(`Conflicts: ${conflicts.slice(0, 8).join(", ")}`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
log.error(`Could not merge ${opts.integrationBranch}: ${msg}`);
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export async function assertMergeRequestNotOpen(cwd, taskFile, allowOpenMr) {
|
|
161
|
+
if (allowOpenMr)
|
|
162
|
+
return true;
|
|
163
|
+
const state = await getMergeRequestStateForTask(cwd, taskFile);
|
|
164
|
+
if (state !== "open")
|
|
165
|
+
return true;
|
|
166
|
+
log.error("Current MR/PR is still open. Merge or close it first, or rerun with --allow-open-mr.");
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
export async function refreshGitContextBaseCommit(taskFile, gitCtx, cwd) {
|
|
170
|
+
const head = await gitHeadCommit(cwd);
|
|
171
|
+
if (head === null || head.length === 0)
|
|
172
|
+
return gitCtx;
|
|
173
|
+
return { ...gitCtx, baseCommit: head };
|
|
174
|
+
}
|
|
175
|
+
export async function prepareGitContextForReship(taskFile, gitCtx) {
|
|
176
|
+
const archived = archiveMergeRequestForReship(gitCtx);
|
|
177
|
+
await upsertGitContext(taskFile, archived);
|
|
178
|
+
return archived;
|
|
179
|
+
}
|
|
180
|
+
export async function loadGitConfigForReship(cwd) {
|
|
181
|
+
try {
|
|
182
|
+
const cfg = await requireGitConfig(cwd);
|
|
183
|
+
return { remote: cfg.remote, integrationBranch: cfg.integrationBranch };
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (e instanceof GitConfigError) {
|
|
187
|
+
log.error(e.message);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -99,6 +99,87 @@ Be factual. Do not invent files not in the diff.`,
|
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
export async function llmCompleteTaskReship(params) {
|
|
103
|
+
const git = await readGitInfo(params.cwd);
|
|
104
|
+
let diffSummary = "";
|
|
105
|
+
if (git.isRepo) {
|
|
106
|
+
const names = await gitDiffNameOnly(params.cwd);
|
|
107
|
+
diffSummary = names.slice(0, 40).join("\n") || "(no diff vs HEAD index yet)";
|
|
108
|
+
}
|
|
109
|
+
const paths = projectPaths(params.cwd);
|
|
110
|
+
const currentState = (await readOptional(join(paths.root, PROJECT_MEMORY_FILES.currentState))) ?? "";
|
|
111
|
+
const existingResult = readSection(params.taskBody, "Result");
|
|
112
|
+
const existingTest = readSection(params.taskBody, "Test Result");
|
|
113
|
+
const messages = [
|
|
114
|
+
{
|
|
115
|
+
role: "system",
|
|
116
|
+
content: `You reship a follow-up on an already Shipped VibeOps TASK. Reply with JSON only:
|
|
117
|
+
{
|
|
118
|
+
"result": "full Result section — preserve prior facts and add a ## Follow-up subsection for this pass",
|
|
119
|
+
"testResult": "full Test Result for this follow-up — commands and outcomes",
|
|
120
|
+
"currentStateMarkdown": "full replacement for docs/project/05-current-state.md OR null to skip",
|
|
121
|
+
"decisionsAppend": "markdown bullets to append under ## Decisions OR null",
|
|
122
|
+
"architectureUpdate": "full replacement for 03-architecture.md OR null if unchanged",
|
|
123
|
+
"logLine": "one line for docs/logs/YYYY-MM-DD.md"
|
|
124
|
+
}
|
|
125
|
+
Be factual. Do not invent files not in the diff.`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
role: "user",
|
|
129
|
+
content: [
|
|
130
|
+
`TASK follow-up reship: ${params.taskId} — ${params.taskTitle}`,
|
|
131
|
+
`File: ${params.taskFileRel}`,
|
|
132
|
+
"",
|
|
133
|
+
"Existing Result:",
|
|
134
|
+
existingResult || "(empty)",
|
|
135
|
+
"",
|
|
136
|
+
"Existing Test Result:",
|
|
137
|
+
existingTest || "(empty)",
|
|
138
|
+
"",
|
|
139
|
+
"Changed paths (working tree / HEAD):",
|
|
140
|
+
diffSummary,
|
|
141
|
+
"",
|
|
142
|
+
"Current project state excerpt:",
|
|
143
|
+
currentState.slice(0, 2000),
|
|
144
|
+
].join("\n"),
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
try {
|
|
148
|
+
const config = await readConfig(params.cwd);
|
|
149
|
+
const preference = getLlmPreferenceFromConfig(config);
|
|
150
|
+
const { text: raw, provider } = await llmCompleteJson(messages, {
|
|
151
|
+
cwd: params.cwd,
|
|
152
|
+
preference,
|
|
153
|
+
});
|
|
154
|
+
const parsed = JSON.parse(extractJsonObject(raw));
|
|
155
|
+
return {
|
|
156
|
+
result: typeof parsed.result === "string" && parsed.result.trim().length > 0
|
|
157
|
+
? parsed.result.trim()
|
|
158
|
+
: `${existingResult}\n\n## Follow-up\n\nReship ${params.taskId}.`,
|
|
159
|
+
testResult: typeof parsed.testResult === "string" && parsed.testResult.trim().length > 0
|
|
160
|
+
? parsed.testResult.trim()
|
|
161
|
+
: existingTest || "Follow-up verification required.",
|
|
162
|
+
currentStateMarkdown: typeof parsed.currentStateMarkdown === "string" &&
|
|
163
|
+
parsed.currentStateMarkdown.trim().length > 0
|
|
164
|
+
? parsed.currentStateMarkdown.trim()
|
|
165
|
+
: null,
|
|
166
|
+
decisionsAppend: typeof parsed.decisionsAppend === "string" && parsed.decisionsAppend.trim().length > 0
|
|
167
|
+
? parsed.decisionsAppend.trim()
|
|
168
|
+
: null,
|
|
169
|
+
architectureUpdate: typeof parsed.architectureUpdate === "string" &&
|
|
170
|
+
parsed.architectureUpdate.trim().length > 0
|
|
171
|
+
? parsed.architectureUpdate.trim()
|
|
172
|
+
: null,
|
|
173
|
+
logLine: typeof parsed.logLine === "string" && parsed.logLine.trim().length > 0
|
|
174
|
+
? parsed.logLine.trim()
|
|
175
|
+
: `${params.taskId} reship follow-up.`,
|
|
176
|
+
provider,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
102
183
|
export async function applyTaskShipMemory(cwd, patch) {
|
|
103
184
|
const paths = projectPaths(cwd);
|
|
104
185
|
const updated = [];
|
package/dist/lib/task.js
CHANGED
|
@@ -331,6 +331,29 @@ export async function updateTaskSection(filePath, title, content) {
|
|
|
331
331
|
await writeText(filePath, updated);
|
|
332
332
|
}
|
|
333
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
|
+
}
|
|
334
357
|
function renderGitContextBlock(ctx) {
|
|
335
358
|
const lines = [
|
|
336
359
|
`- Base Branch: \`${ctx.baseBranch}\``,
|
|
@@ -344,11 +367,39 @@ function renderGitContextBlock(ctx) {
|
|
|
344
367
|
if (typeof ctx.mergeRequestUrl === "string" && ctx.mergeRequestUrl.length > 0) {
|
|
345
368
|
lines.push(`- Merge Request: ${ctx.mergeRequestUrl}`);
|
|
346
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
|
+
}
|
|
347
377
|
if (typeof ctx.pushedAt === "string" && ctx.pushedAt.length > 0) {
|
|
348
378
|
lines.push(`- Pushed At: \`${ctx.pushedAt}\``);
|
|
349
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
|
+
}
|
|
350
386
|
return lines.join("\n");
|
|
351
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
|
+
}
|
|
352
403
|
export async function upsertGitContext(filePath, ctx) {
|
|
353
404
|
const raw = await readText(filePath);
|
|
354
405
|
const block = renderGitContextBlock(ctx);
|
|
@@ -377,6 +428,8 @@ const GIT_CTX_RE = {
|
|
|
377
428
|
doneAt: /^-\s+Done At:\s*`([^`]+)`/m,
|
|
378
429
|
mergeRequestUrl: /^-\s+Merge Request:\s*(.+)$/m,
|
|
379
430
|
pushedAt: /^-\s+Pushed At:\s*`([^`]+)`/m,
|
|
431
|
+
lastReshipAt: /^-\s+Last Reship At:\s*`([^`]+)`/m,
|
|
432
|
+
reshipCount: /^-\s+Reship Count:\s*`(\d+)`/m,
|
|
380
433
|
};
|
|
381
434
|
export async function markGitContextDone(filePath) {
|
|
382
435
|
const ctx = await readGitContext(filePath);
|
|
@@ -398,13 +451,27 @@ export async function readGitContext(filePath) {
|
|
|
398
451
|
const doneAt = content.match(GIT_CTX_RE.doneAt)?.[1];
|
|
399
452
|
const mergeRequestUrl = content.match(GIT_CTX_RE.mergeRequestUrl)?.[1]?.trim();
|
|
400
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);
|
|
401
457
|
const ctx = { baseBranch, baseCommit, taskBranch, startedAt };
|
|
402
458
|
if (typeof doneAt === "string" && doneAt.length > 0)
|
|
403
459
|
ctx.doneAt = doneAt;
|
|
404
460
|
if (typeof mergeRequestUrl === "string" && mergeRequestUrl.length > 0) {
|
|
405
461
|
ctx.mergeRequestUrl = mergeRequestUrl;
|
|
406
462
|
}
|
|
463
|
+
if (previousMergeRequestUrls.length > 0) {
|
|
464
|
+
ctx.previousMergeRequestUrls = previousMergeRequestUrls;
|
|
465
|
+
}
|
|
407
466
|
if (typeof pushedAt === "string" && pushedAt.length > 0)
|
|
408
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
|
+
}
|
|
409
476
|
return ctx;
|
|
410
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`.
|