@goodtek/vibeops 1.0.0 → 1.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 +31 -0
- package/README.md +51 -60
- package/dist/bootstrap/installer.js +38 -4
- package/dist/bootstrap/manifest.js +23 -3
- package/dist/cli.js +64 -15
- package/dist/commands/init.js +279 -114
- package/dist/commands/llm.js +177 -0
- package/dist/commands/status.js +37 -1
- package/dist/commands/task-add.js +33 -6
- package/dist/commands/task-done.js +27 -31
- package/dist/lib/config.js +84 -4
- package/dist/lib/env.js +32 -0
- package/dist/lib/git-config.js +21 -0
- package/dist/lib/git-host.js +12 -0
- package/dist/lib/git-policy.js +67 -0
- package/dist/lib/git-remote.js +108 -0
- package/dist/lib/git.js +43 -0
- package/dist/lib/init-clients.js +55 -0
- package/dist/lib/llm-complete.js +18 -19
- package/dist/lib/llm-connect.js +150 -0
- package/dist/lib/llm-preference.js +102 -0
- package/dist/lib/llm-status.js +19 -0
- package/dist/lib/pr-create.js +66 -0
- package/dist/lib/task-add-llm.js +8 -1
- package/dist/lib/task-done-llm.js +8 -1
- package/dist/lib/task-pr-llm.js +50 -0
- package/dist/lib/task-push-pr.js +131 -0
- package/dist/lib/task-start.js +46 -18
- package/dist/lib/task.js +15 -0
- package/package.json +13 -11
- package/templates/.cursor/rules/02-git-safety.mdc +5 -4
- package/templates/AGENTS.md +2 -1
- package/templates/clients/claude/CLAUDE.md +28 -0
- package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
- package/templates/core/AGENTS.md +44 -0
- package/templates/{.cursor/skills → skills-shared}/implement-task/SKILL.md +1 -1
- package/templates/{.cursor/skills → skills-shared}/plan-task/SKILL.md +3 -3
- package/dist/lib/task-merge.js +0 -115
- /package/templates/{.cursor → clients/cursor/.cursor}/rules/01-task-source-of-truth.mdc +0 -0
- /package/templates/{.cursor → clients/cursor/.cursor}/rules/03-docs-on-done.mdc +0 -0
- /package/templates/{docs → core/docs}/logs/README.md +0 -0
- /package/templates/{docs → core/docs}/project/03-architecture.md +0 -0
- /package/templates/{docs → core/docs}/project/05-current-state.md +0 -0
- /package/templates/{docs → core/docs}/project/06-decisions.md +0 -0
- /package/templates/{docs → core/docs}/tasks/TASK-000-template.md +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readText } from "./filesystem.js";
|
|
2
|
+
import { readConfig } from "./config.js";
|
|
3
|
+
import { detectGitHost, mergeRequestLabel } from "./git-host.js";
|
|
4
|
+
import { requireGitConfig } from "./git-config.js";
|
|
5
|
+
import { gitCommitsAhead, gitDiffNameOnly, gitLogOneline, gitPush, gitRemoteUrl, readGitInfo, } from "./git.js";
|
|
6
|
+
import { dim, log } from "./logger.js";
|
|
7
|
+
import { createMergeRequest, probeMergeRequestCli } from "./pr-create.js";
|
|
8
|
+
import { fallbackTaskPr, generateTaskPrWithLlm } from "./task-pr-llm.js";
|
|
9
|
+
import { readGitContext, readTaskFile, upsertGitContext } from "./task.js";
|
|
10
|
+
async function buildDiffSummary(cwd, gitCtx) {
|
|
11
|
+
const files = await gitDiffNameOnly(cwd, `${gitCtx.baseCommit}..HEAD`);
|
|
12
|
+
const commits = await gitLogOneline(cwd, `${gitCtx.baseCommit}..HEAD`);
|
|
13
|
+
const lines = [];
|
|
14
|
+
if (commits.length > 0) {
|
|
15
|
+
lines.push("Commits:");
|
|
16
|
+
for (const c of commits.slice(0, 20)) {
|
|
17
|
+
lines.push(`- ${c.sha} ${c.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (commits.length > 20)
|
|
20
|
+
lines.push(`- … and ${commits.length - 20} more`);
|
|
21
|
+
}
|
|
22
|
+
if (files.length > 0) {
|
|
23
|
+
lines.push("", "Files:");
|
|
24
|
+
for (const f of files.slice(0, 40))
|
|
25
|
+
lines.push(`- ${f}`);
|
|
26
|
+
if (files.length > 40)
|
|
27
|
+
lines.push(`- … and ${files.length - 40} more`);
|
|
28
|
+
}
|
|
29
|
+
return lines.join("\n") || "(no commits since task start)";
|
|
30
|
+
}
|
|
31
|
+
function isExistingMergeRequestUrl(url) {
|
|
32
|
+
if (typeof url !== "string" || url.length === 0)
|
|
33
|
+
return false;
|
|
34
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
35
|
+
}
|
|
36
|
+
/** Push task branch and open MR/PR (idempotent when URL already in Git Context). */
|
|
37
|
+
export async function finishTaskWithPullRequest(opts) {
|
|
38
|
+
let gitCfg = await requireGitConfig(opts.cwd);
|
|
39
|
+
const gitCtx = await readGitContext(opts.taskFile);
|
|
40
|
+
if (gitCtx === null) {
|
|
41
|
+
log.warn("No Git Context on TASK — push and merge request skipped.");
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
44
|
+
if (opts.dryRun) {
|
|
45
|
+
log.info(dim(`would git push -u ${gitCfg.remote} ${gitCtx.taskBranch} and open ${mergeRequestLabel(gitCfg.host)} → ${gitCtx.baseBranch}`));
|
|
46
|
+
return { ok: true, mergeRequestUrl: "(dry-run)" };
|
|
47
|
+
}
|
|
48
|
+
const remoteUrl = await gitRemoteUrl(opts.cwd, gitCfg.remote);
|
|
49
|
+
if (!remoteUrl) {
|
|
50
|
+
log.error(`Remote "${gitCfg.remote}" is not configured. Add it or re-run vibeops init.`);
|
|
51
|
+
return { ok: false };
|
|
52
|
+
}
|
|
53
|
+
const detectedHost = detectGitHost(remoteUrl);
|
|
54
|
+
if (detectedHost !== null && detectedHost !== gitCfg.host) {
|
|
55
|
+
log.warn(`Remote host (${detectedHost}) differs from .vibeops.json (${gitCfg.host}). Using ${detectedHost} for MR/PR.`);
|
|
56
|
+
gitCfg = { ...gitCfg, host: detectedHost };
|
|
57
|
+
}
|
|
58
|
+
const git = await readGitInfo(opts.cwd);
|
|
59
|
+
if (git.branch !== gitCtx.taskBranch) {
|
|
60
|
+
log.warn(`Current branch is ${git.branch ?? "(detached)"}, not ${gitCtx.taskBranch}. Push may fail.`);
|
|
61
|
+
}
|
|
62
|
+
if (isExistingMergeRequestUrl(gitCtx.mergeRequestUrl)) {
|
|
63
|
+
log.info(dim(`Merge request already recorded: ${gitCtx.mergeRequestUrl}`));
|
|
64
|
+
return { ok: true, mergeRequestUrl: gitCtx.mergeRequestUrl, pushed: true };
|
|
65
|
+
}
|
|
66
|
+
const ahead = await gitCommitsAhead(opts.cwd, gitCtx.baseCommit, "HEAD");
|
|
67
|
+
if (ahead === 0) {
|
|
68
|
+
log.warn("No commits ahead of base — pushing branch anyway.");
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await gitPush(opts.cwd, gitCfg.remote, gitCtx.taskBranch, true);
|
|
72
|
+
log.ok(`Pushed ${gitCtx.taskBranch} → ${gitCfg.remote}`);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
76
|
+
log.error(`git push failed: ${msg}`);
|
|
77
|
+
return { ok: false };
|
|
78
|
+
}
|
|
79
|
+
const pushedAt = new Date().toISOString();
|
|
80
|
+
if (opts.skipPr === true) {
|
|
81
|
+
await upsertGitContext(opts.taskFile, { ...gitCtx, pushedAt });
|
|
82
|
+
log.info(dim("Merge request creation skipped (--no-pr)."));
|
|
83
|
+
return { ok: true, pushed: true };
|
|
84
|
+
}
|
|
85
|
+
const cliOk = await probeMergeRequestCli(gitCfg.host);
|
|
86
|
+
if (!cliOk) {
|
|
87
|
+
const tool = gitCfg.host === "gitlab" ? "glab" : "gh";
|
|
88
|
+
await upsertGitContext(opts.taskFile, { ...gitCtx, pushedAt });
|
|
89
|
+
log.warn(`${tool} CLI not found — create the ${mergeRequestLabel(gitCfg.host)} manually: ${gitCtx.taskBranch} → ${gitCtx.baseBranch}`);
|
|
90
|
+
return { ok: true, pushed: true };
|
|
91
|
+
}
|
|
92
|
+
const config = await readConfig(opts.cwd);
|
|
93
|
+
const preference = config?.llm?.provider ?? "auto";
|
|
94
|
+
const meta = await readTaskFile(opts.taskFile);
|
|
95
|
+
const taskBody = await readText(opts.taskFile);
|
|
96
|
+
const diffSummary = await buildDiffSummary(opts.cwd, gitCtx);
|
|
97
|
+
const prInput = {
|
|
98
|
+
taskId: meta.id,
|
|
99
|
+
title: meta.title,
|
|
100
|
+
taskBody,
|
|
101
|
+
diffSummary,
|
|
102
|
+
baseBranch: gitCtx.baseBranch,
|
|
103
|
+
headBranch: gitCtx.taskBranch,
|
|
104
|
+
};
|
|
105
|
+
const llmPr = await generateTaskPrWithLlm(prInput, opts.cwd, preference);
|
|
106
|
+
const { prTitle, prBody } = llmPr ?? fallbackTaskPr(prInput);
|
|
107
|
+
try {
|
|
108
|
+
const { url } = await createMergeRequest({
|
|
109
|
+
cwd: opts.cwd,
|
|
110
|
+
host: gitCfg.host,
|
|
111
|
+
baseBranch: gitCtx.baseBranch,
|
|
112
|
+
headBranch: gitCtx.taskBranch,
|
|
113
|
+
title: prTitle,
|
|
114
|
+
body: prBody,
|
|
115
|
+
});
|
|
116
|
+
log.ok(`${mergeRequestLabel(gitCfg.host)}: ${url}`);
|
|
117
|
+
await upsertGitContext(opts.taskFile, {
|
|
118
|
+
...gitCtx,
|
|
119
|
+
mergeRequestUrl: url,
|
|
120
|
+
pushedAt,
|
|
121
|
+
});
|
|
122
|
+
return { ok: true, mergeRequestUrl: url, pushed: true };
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
126
|
+
log.error(`Could not create merge request: ${msg}`);
|
|
127
|
+
await upsertGitContext(opts.taskFile, { ...gitCtx, pushedAt });
|
|
128
|
+
log.info(dim(`Branch was pushed. Create MR manually: ${gitCtx.taskBranch} → ${gitCtx.baseBranch}`));
|
|
129
|
+
return { ok: false, pushed: true };
|
|
130
|
+
}
|
|
131
|
+
}
|
package/dist/lib/task-start.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
2
|
import { branchNameForTaskFile, readGitContext, readTaskFile, updateInlineStatus, upsertGitContext, } from "./task.js";
|
|
3
|
-
import {
|
|
3
|
+
import { gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitGovernanceOnlyDirty, gitHeadCommit, gitPullFastForwardOnly, gitRemoteBranchExists, gitSwitchToBranch, gitRemoteUrl, readGitInfo, } from "./git.js";
|
|
4
4
|
import { cyan, dim, log } from "./logger.js";
|
|
5
5
|
function relOrAbs(root, p) {
|
|
6
6
|
const r = relative(root, p);
|
|
7
7
|
return r === "" ? "." : r.startsWith("..") ? p : r;
|
|
8
8
|
}
|
|
9
|
-
/** Create or resume task branch and mark TASK In Progress
|
|
9
|
+
/** Create or resume task branch from integration branch and mark TASK In Progress. */
|
|
10
10
|
export async function startTaskBranch(opts) {
|
|
11
11
|
const cwd = resolve(opts.cwd);
|
|
12
12
|
const taskFile = opts.taskFile;
|
|
13
|
+
const integrationBranch = opts.integrationBranch;
|
|
14
|
+
const remote = opts.remote ?? "origin";
|
|
13
15
|
const meta = await readTaskFile(taskFile);
|
|
14
16
|
const git = await readGitInfo(cwd);
|
|
15
17
|
if (!git.isRepo) {
|
|
@@ -17,7 +19,7 @@ export async function startTaskBranch(opts) {
|
|
|
17
19
|
log.info(dim("dry-run — would require a git repository"));
|
|
18
20
|
return true;
|
|
19
21
|
}
|
|
20
|
-
log.error("Not a git repository. Run `vibeops init
|
|
22
|
+
log.error("Not a git repository. Run `vibeops init` with Git setup first.");
|
|
21
23
|
return false;
|
|
22
24
|
}
|
|
23
25
|
if (git.dirty === true && opts.allowDirty !== true) {
|
|
@@ -28,29 +30,55 @@ export async function startTaskBranch(opts) {
|
|
|
28
30
|
}
|
|
29
31
|
log.warn("Only governance paths are dirty — proceeding.");
|
|
30
32
|
}
|
|
31
|
-
const baseBranch = git.branch ?? (await detectDefaultBranch(cwd)) ?? "main";
|
|
32
|
-
const baseCommit = (await gitHeadCommit(cwd)) ?? "";
|
|
33
|
-
if (baseCommit.length === 0) {
|
|
34
|
-
log.error("No commits on HEAD yet. Create an initial commit first.");
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
33
|
const taskBranch = branchNameForTaskFile(taskFile);
|
|
38
|
-
const ctx = {
|
|
39
|
-
baseBranch,
|
|
40
|
-
baseCommit,
|
|
41
|
-
taskBranch,
|
|
42
|
-
startedAt: new Date().toISOString(),
|
|
43
|
-
};
|
|
44
34
|
const existingCtx = await readGitContext(taskFile);
|
|
45
35
|
const branchExists = await gitBranchExists(cwd, taskBranch);
|
|
46
|
-
log.info(` ${dim("task branch")} ${cyan(taskBranch)}`);
|
|
47
|
-
log.info(` ${dim("base")} ${baseBranch} @ ${baseCommit.slice(0, 7)}`);
|
|
48
36
|
if (opts.dryRun) {
|
|
37
|
+
log.info(` ${dim("integration")} ${integrationBranch}`);
|
|
38
|
+
log.info(` ${dim("task branch")} ${cyan(taskBranch)}`);
|
|
49
39
|
log.info(dim(branchExists
|
|
50
40
|
? `dry-run — would git switch ${taskBranch}`
|
|
51
|
-
: `dry-run — would git switch -c ${taskBranch}`));
|
|
41
|
+
: `dry-run — would checkout ${integrationBranch}, then git switch -c ${taskBranch}`));
|
|
52
42
|
return true;
|
|
53
43
|
}
|
|
44
|
+
if (!branchExists && git.branch !== integrationBranch) {
|
|
45
|
+
const ok = await gitSwitchToBranch(cwd, integrationBranch, remote);
|
|
46
|
+
if (!ok) {
|
|
47
|
+
log.error(`Integration branch "${integrationBranch}" not found locally or on ${remote}. Run vibeops init or create the branch.`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Ensure integration branch is up-to-date before creating a new task branch.
|
|
52
|
+
if (!branchExists) {
|
|
53
|
+
const remoteUrl = await gitRemoteUrl(cwd, remote);
|
|
54
|
+
if (remoteUrl) {
|
|
55
|
+
const hasRemoteBranch = await gitRemoteBranchExists(cwd, remote, integrationBranch);
|
|
56
|
+
if (hasRemoteBranch) {
|
|
57
|
+
try {
|
|
58
|
+
await gitPullFastForwardOnly(cwd, remote, integrationBranch);
|
|
59
|
+
log.info(dim(`Pulled latest ${remote}/${integrationBranch} (--ff-only).`));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
log.warn(`Could not fast-forward pull ${remote}/${integrationBranch}. Resolve manually, then rerun task add.`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const baseBranch = integrationBranch;
|
|
69
|
+
const baseCommit = (await gitHeadCommit(cwd)) ?? "";
|
|
70
|
+
if (baseCommit.length === 0 && !branchExists) {
|
|
71
|
+
log.error("No commits on integration branch. Create an initial commit first.");
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const ctx = {
|
|
75
|
+
baseBranch,
|
|
76
|
+
baseCommit: branchExists && existingCtx ? existingCtx.baseCommit : baseCommit,
|
|
77
|
+
taskBranch,
|
|
78
|
+
startedAt: existingCtx?.startedAt ?? new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
log.info(` ${dim("integration")} ${baseBranch} @ ${ctx.baseCommit.slice(0, 7)}`);
|
|
81
|
+
log.info(` ${dim("task branch")} ${cyan(taskBranch)}`);
|
|
54
82
|
if (branchExists) {
|
|
55
83
|
await gitCheckout(cwd, taskBranch);
|
|
56
84
|
if (meta.status === "planned") {
|
package/dist/lib/task.js
CHANGED
|
@@ -353,6 +353,12 @@ function renderGitContextBlock(ctx) {
|
|
|
353
353
|
if (typeof ctx.doneAt === "string" && ctx.doneAt.length > 0) {
|
|
354
354
|
lines.push(`- Done At: \`${ctx.doneAt}\``);
|
|
355
355
|
}
|
|
356
|
+
if (typeof ctx.mergeRequestUrl === "string" && ctx.mergeRequestUrl.length > 0) {
|
|
357
|
+
lines.push(`- Merge Request: ${ctx.mergeRequestUrl}`);
|
|
358
|
+
}
|
|
359
|
+
if (typeof ctx.pushedAt === "string" && ctx.pushedAt.length > 0) {
|
|
360
|
+
lines.push(`- Pushed At: \`${ctx.pushedAt}\``);
|
|
361
|
+
}
|
|
356
362
|
return lines.join("\n");
|
|
357
363
|
}
|
|
358
364
|
export async function upsertGitContext(filePath, ctx) {
|
|
@@ -381,6 +387,8 @@ const GIT_CTX_RE = {
|
|
|
381
387
|
taskBranch: /^-\s+Task Branch:\s*`([^`]+)`/m,
|
|
382
388
|
startedAt: /^-\s+Started At:\s*`([^`]+)`/m,
|
|
383
389
|
doneAt: /^-\s+Done At:\s*`([^`]+)`/m,
|
|
390
|
+
mergeRequestUrl: /^-\s+Merge Request:\s*(.+)$/m,
|
|
391
|
+
pushedAt: /^-\s+Pushed At:\s*`([^`]+)`/m,
|
|
384
392
|
};
|
|
385
393
|
export async function markGitContextDone(filePath) {
|
|
386
394
|
const ctx = await readGitContext(filePath);
|
|
@@ -400,8 +408,15 @@ export async function readGitContext(filePath) {
|
|
|
400
408
|
if (!baseBranch || !baseCommit || !taskBranch || !startedAt)
|
|
401
409
|
return null;
|
|
402
410
|
const doneAt = content.match(GIT_CTX_RE.doneAt)?.[1];
|
|
411
|
+
const mergeRequestUrl = content.match(GIT_CTX_RE.mergeRequestUrl)?.[1]?.trim();
|
|
412
|
+
const pushedAt = content.match(GIT_CTX_RE.pushedAt)?.[1];
|
|
403
413
|
const ctx = { baseBranch, baseCommit, taskBranch, startedAt };
|
|
404
414
|
if (typeof doneAt === "string" && doneAt.length > 0)
|
|
405
415
|
ctx.doneAt = doneAt;
|
|
416
|
+
if (typeof mergeRequestUrl === "string" && mergeRequestUrl.length > 0) {
|
|
417
|
+
ctx.mergeRequestUrl = mergeRequestUrl;
|
|
418
|
+
}
|
|
419
|
+
if (typeof pushedAt === "string" && pushedAt.length > 0)
|
|
420
|
+
ctx.pushedAt = pushedAt;
|
|
406
421
|
return ctx;
|
|
407
422
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodtek/vibeops",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Minimal CLI rails for Cursor-based TASK-driven vibe coding.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@9.15.9",
|
|
6
7
|
"author": "VibeOps contributors",
|
|
7
8
|
"license": "MIT",
|
|
8
9
|
"repository": {
|
|
@@ -29,6 +30,16 @@
|
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=20"
|
|
31
32
|
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "tsx src/cli.ts",
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"start": "node dist/cli.js",
|
|
38
|
+
"smoke": "pnpm typecheck && pnpm build && node scripts/smoke.mjs",
|
|
39
|
+
"clean": "rm -rf dist",
|
|
40
|
+
"prepack": "pnpm clean && pnpm build",
|
|
41
|
+
"publish:dry": "pnpm publish --dry-run --no-git-checks"
|
|
42
|
+
},
|
|
32
43
|
"keywords": [
|
|
33
44
|
"cursor",
|
|
34
45
|
"ai",
|
|
@@ -47,14 +58,5 @@
|
|
|
47
58
|
"@types/node": "^20.14.0",
|
|
48
59
|
"tsx": "^4.19.0",
|
|
49
60
|
"typescript": "^5.5.4"
|
|
50
|
-
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"dev": "tsx src/cli.ts",
|
|
53
|
-
"build": "tsc -p tsconfig.json",
|
|
54
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
-
"start": "node dist/cli.js",
|
|
56
|
-
"smoke": "pnpm typecheck && pnpm build && node scripts/smoke.mjs",
|
|
57
|
-
"clean": "rm -rf dist",
|
|
58
|
-
"publish:dry": "pnpm publish --dry-run --no-git-checks"
|
|
59
61
|
}
|
|
60
|
-
}
|
|
62
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Git safety for task branches and
|
|
2
|
+
description: Git safety for task branches and merge requests
|
|
3
3
|
alwaysApply: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Git safety
|
|
7
7
|
|
|
8
|
-
-
|
|
8
|
+
- Branch policy lives in `.vibeops.json` (`integrationBranch`, `productionBranch`).
|
|
9
|
+
- `vibeops task add` creates `task/*` from the **integration** branch (e.g. `develop`).
|
|
9
10
|
- Include the TASK id in commit messages, e.g. `feat(task-015): short title`.
|
|
10
|
-
-
|
|
11
|
-
-
|
|
11
|
+
- **`vibeops task done`** pushes the branch and opens an MR/PR — it does **not** merge locally.
|
|
12
|
+
- Merge on GitHub/GitLab; CI deploys after merge. No force-push to shared branches.
|
package/templates/AGENTS.md
CHANGED
|
@@ -24,8 +24,9 @@ vibeops init # once
|
|
|
24
24
|
vibeops task add # new TASK + branch
|
|
25
25
|
# Plan in Cursor Ask: @docs/tasks/TASK-NNN-….md
|
|
26
26
|
# Build in Cursor Agent: same file + Scope / Acceptance Criteria
|
|
27
|
-
vibeops task done # close TASK, update project docs,
|
|
27
|
+
vibeops task done # close TASK, update project docs, push + open MR/PR
|
|
28
28
|
vibeops status # briefing
|
|
29
|
+
vibeops llm connect # set up LLM providers and pick default
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
## Rules
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} — Claude Code
|
|
2
|
+
|
|
3
|
+
Read **`AGENTS.md`** first (VibeOps workflow and source of truth).
|
|
4
|
+
|
|
5
|
+
## Skills (project)
|
|
6
|
+
|
|
7
|
+
| Skill | When |
|
|
8
|
+
|-------|------|
|
|
9
|
+
| `/plan-task` | Refine `docs/tasks/TASK-NNN-*.md` before coding |
|
|
10
|
+
| `/implement-task` | Build per TASK Scope and Acceptance Criteria |
|
|
11
|
+
|
|
12
|
+
Skills live in `.claude/skills/`.
|
|
13
|
+
|
|
14
|
+
## Session start
|
|
15
|
+
|
|
16
|
+
1. `docs/project/05-current-state.md`
|
|
17
|
+
2. The **full** current `docs/tasks/TASK-NNN-*.md` file
|
|
18
|
+
3. `docs/project/03-architecture.md` and `06-decisions.md` when relevant
|
|
19
|
+
|
|
20
|
+
## Git
|
|
21
|
+
|
|
22
|
+
- Branch: `task/*` from `vibeops task add`
|
|
23
|
+
- Commit messages include the TASK id, e.g. `feat(task-015): short title`
|
|
24
|
+
- Do not merge to main unless the human runs `vibeops task done` or asks
|
|
25
|
+
|
|
26
|
+
## Finishing
|
|
27
|
+
|
|
28
|
+
Fill **Result** and **Test Result** in the TASK file, then tell the human to run `vibeops task done TASK-NNN`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Git safety for task branches and commits
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git safety
|
|
7
|
+
|
|
8
|
+
- Use branch pattern `task/*` created by `vibeops task add`.
|
|
9
|
+
- Include the TASK id in commit messages, e.g. `feat(task-015): short title`.
|
|
10
|
+
- **No force-push** to shared branches (`main`, `master`).
|
|
11
|
+
- Do not merge or push unless the human runs `vibeops task done` or asks explicitly.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} — AI agent guide
|
|
2
|
+
|
|
3
|
+
> Installed by **VibeOps**. Agents read this repo and `docs/tasks/`, not chat history.
|
|
4
|
+
|
|
5
|
+
## Source of truth
|
|
6
|
+
|
|
7
|
+
| What | Where |
|
|
8
|
+
|------|--------|
|
|
9
|
+
| Current work | `docs/tasks/TASK-NNN-*.md` |
|
|
10
|
+
| Project memory | `docs/project/05-current-state.md`, `03-architecture.md`, `06-decisions.md` |
|
|
11
|
+
| Change history | Git commits on task branches |
|
|
12
|
+
| **Not** trusted | Chat history, Slack |
|
|
13
|
+
|
|
14
|
+
## Before you code
|
|
15
|
+
|
|
16
|
+
1. `docs/project/05-current-state.md`
|
|
17
|
+
2. `docs/project/03-architecture.md` and `06-decisions.md` when relevant
|
|
18
|
+
3. **The full current TASK file** under `docs/tasks/`
|
|
19
|
+
|
|
20
|
+
## Workflow (human + CLI)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
vibeops init # once (pick Cursor / Claude Code / Codex packs)
|
|
24
|
+
vibeops task add # new TASK + branch
|
|
25
|
+
# Plan: refine TASK file with your agent (see client skills below)
|
|
26
|
+
# Build: implement per TASK Scope / Acceptance Criteria
|
|
27
|
+
vibeops task done # close TASK, update project docs, merge
|
|
28
|
+
vibeops status # briefing
|
|
29
|
+
vibeops llm connect # LLM for task add / task done
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Client packs (installed at init)
|
|
33
|
+
|
|
34
|
+
| Tool | Rules / guide | Skills |
|
|
35
|
+
|------|----------------|--------|
|
|
36
|
+
| **Cursor** | `.cursor/rules/` | `.cursor/skills/` (`plan-task`, `implement-task`) |
|
|
37
|
+
| **Claude Code** | `CLAUDE.md` | `.claude/skills/` |
|
|
38
|
+
| **Codex CLI** | `AGENTS.md` (this file) | `.agents/skills/` |
|
|
39
|
+
|
|
40
|
+
## Rules (all agents)
|
|
41
|
+
|
|
42
|
+
- **One TASK at a time** — stay inside Scope and Acceptance Criteria.
|
|
43
|
+
- **Search** before adding files; reuse existing patterns.
|
|
44
|
+
- Fill **Result** and **Test Result** before `vibeops task done`.
|
|
@@ -8,7 +8,7 @@ disable-model-invocation: true
|
|
|
8
8
|
|
|
9
9
|
## Read first
|
|
10
10
|
|
|
11
|
-
1. `AGENTS.md` and `.cursor/rules
|
|
11
|
+
1. `AGENTS.md` and your agent rules (e.g. `.cursor/rules/`, `CLAUDE.md`)
|
|
12
12
|
2. `docs/project/05-current-state.md`, `03-architecture.md`, `06-decisions.md`
|
|
13
13
|
3. The TASK file referenced by the human
|
|
14
14
|
|
|
@@ -6,14 +6,14 @@ disable-model-invocation: true
|
|
|
6
6
|
|
|
7
7
|
# Plan a TASK
|
|
8
8
|
|
|
9
|
-
The human opened a TASK file (e.g.
|
|
9
|
+
The human opened a TASK file (e.g. `docs/tasks/TASK-015-….md`).
|
|
10
10
|
|
|
11
11
|
## Instructions
|
|
12
12
|
|
|
13
|
-
1. Read the TASK file
|
|
13
|
+
1. Read `AGENTS.md`, your agent rules, and the TASK file; read `docs/project/05-current-state.md`.
|
|
14
14
|
2. Ask only what is missing to make **Scope**, **Acceptance Criteria**, and **Test Plan** verifiable.
|
|
15
15
|
3. Edit the TASK file in place — do not create a second TASK id.
|
|
16
16
|
4. Keep **Out of Scope** explicit.
|
|
17
17
|
5. Do not write application code in this step unless the human asks.
|
|
18
18
|
|
|
19
|
-
When planning is enough, tell the human to
|
|
19
|
+
When planning is enough, tell the human to implement per the TASK (Agent mode / Codex / Claude Code as they prefer).
|
package/dist/lib/task-merge.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { gitBranchExists, gitCheckout, gitDeleteBranch, gitGovernanceOnlyDirty, gitRemoteUrl, readGitInfo, runGit, } from "./git.js";
|
|
2
|
-
import { bold, cyan, dim, log } from "./logger.js";
|
|
3
|
-
function mergeCommitMessage(taskId, taskTitle, taskBranch) {
|
|
4
|
-
const slug = taskTitle.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim() || taskId;
|
|
5
|
-
return `merge(${taskId.toLowerCase()}): ${slug} (${taskBranch})`;
|
|
6
|
-
}
|
|
7
|
-
async function gitPush(cwd, remote, ref, setUpstream) {
|
|
8
|
-
const args = ["push"];
|
|
9
|
-
if (setUpstream)
|
|
10
|
-
args.push("-u");
|
|
11
|
-
args.push(remote, ref);
|
|
12
|
-
await runGit(cwd, args);
|
|
13
|
-
}
|
|
14
|
-
export async function mergeTaskBranch(opts) {
|
|
15
|
-
const remote = opts.remote ?? "origin";
|
|
16
|
-
const { cwd, gitCtx, taskId, taskTitle } = opts;
|
|
17
|
-
const { baseBranch, taskBranch } = gitCtx;
|
|
18
|
-
if (baseBranch === taskBranch) {
|
|
19
|
-
log.error("Task branch matches base branch; nothing to merge.");
|
|
20
|
-
return { ok: false };
|
|
21
|
-
}
|
|
22
|
-
const git = await readGitInfo(cwd);
|
|
23
|
-
if (!git.isRepo) {
|
|
24
|
-
log.error("Not a git repository.");
|
|
25
|
-
return { ok: false };
|
|
26
|
-
}
|
|
27
|
-
const branchExists = await gitBranchExists(cwd, taskBranch);
|
|
28
|
-
if (!branchExists) {
|
|
29
|
-
log.error(`Local task branch not found: ${taskBranch}`);
|
|
30
|
-
return { ok: false };
|
|
31
|
-
}
|
|
32
|
-
const remoteUrl = await gitRemoteUrl(cwd, remote);
|
|
33
|
-
const hasRemote = remoteUrl !== null;
|
|
34
|
-
if (opts.dryRun) {
|
|
35
|
-
log.info(bold("dry-run — would merge:"));
|
|
36
|
-
log.info(` · checkout ${baseBranch}`);
|
|
37
|
-
log.info(` · merge --no-ff ${taskBranch}`);
|
|
38
|
-
if (hasRemote)
|
|
39
|
-
log.info(` · push ${remote} (optional)`);
|
|
40
|
-
log.info(` · delete local ${taskBranch}`);
|
|
41
|
-
return { ok: true };
|
|
42
|
-
}
|
|
43
|
-
if (git.dirty === true && opts.allowDirty !== true) {
|
|
44
|
-
const gov = await gitGovernanceOnlyDirty(cwd);
|
|
45
|
-
if (!gov.onlyGovernance) {
|
|
46
|
-
log.error("Working tree is dirty. Commit or use --allow-dirty.");
|
|
47
|
-
return { ok: false };
|
|
48
|
-
}
|
|
49
|
-
log.warn("Dirty tree is governance-only — proceeding.");
|
|
50
|
-
}
|
|
51
|
-
log.step(bold(`Merging ${cyan(taskBranch)} → ${cyan(baseBranch)}`));
|
|
52
|
-
try {
|
|
53
|
-
if (hasRemote) {
|
|
54
|
-
try {
|
|
55
|
-
await gitPush(cwd, remote, taskBranch, true);
|
|
56
|
-
await runGit(cwd, ["fetch", remote]);
|
|
57
|
-
}
|
|
58
|
-
catch (e) {
|
|
59
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
60
|
-
log.warn(`Push skipped (${msg}). Continuing with local merge.`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
await gitCheckout(cwd, baseBranch);
|
|
64
|
-
if (hasRemote) {
|
|
65
|
-
try {
|
|
66
|
-
await runGit(cwd, ["pull", remote, baseBranch]);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
log.warn(dim(`Could not pull ${remote}/${baseBranch} — merging locally.`));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
await runGit(cwd, [
|
|
73
|
-
"merge",
|
|
74
|
-
"--no-ff",
|
|
75
|
-
taskBranch,
|
|
76
|
-
"-m",
|
|
77
|
-
mergeCommitMessage(taskId, taskTitle, taskBranch),
|
|
78
|
-
]);
|
|
79
|
-
if (hasRemote) {
|
|
80
|
-
try {
|
|
81
|
-
await gitPush(cwd, remote, baseBranch, false);
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
log.warn(dim(`Merge commit created locally; push ${remote} ${baseBranch} manually.`));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
log.ok(`Merged into ${baseBranch}`);
|
|
88
|
-
}
|
|
89
|
-
catch (e) {
|
|
90
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
91
|
-
log.error(`Merge failed: ${msg}`);
|
|
92
|
-
return { ok: false };
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
await gitDeleteBranch(cwd, taskBranch, { force: false });
|
|
96
|
-
log.ok(`Deleted local branch ${taskBranch}`);
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
try {
|
|
100
|
-
await gitDeleteBranch(cwd, taskBranch, { force: true });
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
log.warn(`Could not delete local branch ${taskBranch}.`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (hasRemote) {
|
|
107
|
-
try {
|
|
108
|
-
await runGit(cwd, ["push", remote, "--delete", taskBranch]);
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
log.warn(dim(`Remote branch ${remote}/${taskBranch} may already be gone.`));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return { ok: true };
|
|
115
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|