@goodtek/vibeops 1.0.0 → 1.1.2

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +51 -60
  3. package/dist/bootstrap/installer.js +38 -4
  4. package/dist/bootstrap/manifest.js +17 -7
  5. package/dist/cli.js +64 -15
  6. package/dist/commands/init.js +279 -114
  7. package/dist/commands/llm.js +177 -0
  8. package/dist/commands/status.js +37 -1
  9. package/dist/commands/task-add.js +33 -6
  10. package/dist/commands/task-done.js +100 -31
  11. package/dist/lib/config.js +84 -4
  12. package/dist/lib/env.js +32 -0
  13. package/dist/lib/git-config.js +21 -0
  14. package/dist/lib/git-host.js +12 -0
  15. package/dist/lib/git-policy.js +67 -0
  16. package/dist/lib/git-remote.js +108 -0
  17. package/dist/lib/git.js +43 -0
  18. package/dist/lib/init-clients.js +55 -0
  19. package/dist/lib/llm-complete.js +18 -19
  20. package/dist/lib/llm-connect.js +150 -0
  21. package/dist/lib/llm-preference.js +102 -0
  22. package/dist/lib/llm-status.js +19 -0
  23. package/dist/lib/pr-create.js +66 -0
  24. package/dist/lib/task-add-llm.js +8 -1
  25. package/dist/lib/task-done-llm.js +8 -1
  26. package/dist/lib/task-pr-llm.js +50 -0
  27. package/dist/lib/task-push-pr.js +131 -0
  28. package/dist/lib/task-start.js +46 -18
  29. package/dist/lib/task.js +15 -0
  30. package/package.json +1 -1
  31. package/templates/clients/claude/CLAUDE.md +28 -0
  32. package/templates/{AGENTS.md → core/AGENTS.md} +16 -9
  33. package/templates/{.cursor/skills → skills-shared}/implement-task/SKILL.md +1 -1
  34. package/templates/{.cursor/skills → skills-shared}/plan-task/SKILL.md +3 -3
  35. package/dist/lib/task-merge.js +0 -115
  36. /package/templates/{.cursor → clients/cursor/.cursor}/rules/01-task-source-of-truth.mdc +0 -0
  37. /package/templates/{.cursor → clients/cursor/.cursor}/rules/02-git-safety.mdc +0 -0
  38. /package/templates/{.cursor → clients/cursor/.cursor}/rules/03-docs-on-done.mdc +0 -0
  39. /package/templates/{docs → core/docs}/logs/README.md +0 -0
  40. /package/templates/{docs → core/docs}/project/03-architecture.md +0 -0
  41. /package/templates/{docs → core/docs}/project/05-current-state.md +0 -0
  42. /package/templates/{docs → core/docs}/project/06-decisions.md +0 -0
  43. /package/templates/{docs → core/docs}/tasks/TASK-000-template.md +0 -0
@@ -0,0 +1,50 @@
1
+ import { llmCompleteJson } from "./llm-complete.js";
2
+ const SYSTEM = `You write pull/merge request titles and bodies for a software task.
3
+ Output valid JSON only: { "title": string, "body": string }.
4
+ Title: under 72 chars, include task id.
5
+ Body: markdown with Summary, Changes, Test plan sections. Facts only from the input.`;
6
+ export async function generateTaskPrWithLlm(input, cwd, preference) {
7
+ const user = [
8
+ `Task: ${input.taskId}`,
9
+ `Title: ${input.title}`,
10
+ `Base branch: ${input.baseBranch}`,
11
+ `Head branch: ${input.headBranch}`,
12
+ "",
13
+ "## TASK",
14
+ input.taskBody.slice(0, 12000),
15
+ "",
16
+ "## Diff summary",
17
+ input.diffSummary.slice(0, 8000) || "(no diff captured)",
18
+ ].join("\n");
19
+ const messages = [
20
+ { role: "system", content: SYSTEM },
21
+ { role: "user", content: user },
22
+ ];
23
+ try {
24
+ const { text: raw } = await llmCompleteJson(messages, { cwd, preference });
25
+ const parsed = JSON.parse(raw);
26
+ const prTitle = typeof parsed.title === "string" ? parsed.title.trim() : "";
27
+ const prBody = typeof parsed.body === "string" ? parsed.body.trim() : "";
28
+ if (!prTitle || !prBody)
29
+ return null;
30
+ return { prTitle, prBody };
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ export function fallbackTaskPr(input) {
37
+ const prTitle = `${input.taskId}: ${input.title}`.slice(0, 72);
38
+ const prBody = [
39
+ "## Summary",
40
+ input.title,
41
+ "",
42
+ "## Changes",
43
+ input.diffSummary || "(see commits on branch)",
44
+ "",
45
+ "## Test plan",
46
+ "- [ ] Run project test/lint commands",
47
+ "- [ ] Review TASK acceptance criteria",
48
+ ].join("\n");
49
+ return { prTitle, prBody };
50
+ }
@@ -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
+ }
@@ -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 { detectDefaultBranch, gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitGovernanceOnlyDirty, gitHeadCommit, readGitInfo, } from "./git.js";
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 (used by `task add`). */
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 --git` first.");
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodtek/vibeops",
3
- "version": "1.0.0",
3
+ "version": "1.1.2",
4
4
  "description": "Minimal CLI rails for Cursor-based TASK-driven vibe coding.",
5
5
  "type": "module",
6
6
  "author": "VibeOps contributors",
@@ -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`.
@@ -1,6 +1,6 @@
1
1
  # {{PROJECT_NAME}} — AI agent guide
2
2
 
3
- > Installed by **VibeOps**. Cursor reads this repo, not chat history.
3
+ > Installed by **VibeOps**. Agents read this repo and `docs/tasks/`, not chat history.
4
4
 
5
5
  ## Source of truth
6
6
 
@@ -9,7 +9,7 @@
9
9
  | Current work | `docs/tasks/TASK-NNN-*.md` |
10
10
  | Project memory | `docs/project/05-current-state.md`, `03-architecture.md`, `06-decisions.md` |
11
11
  | Change history | Git commits on task branches |
12
- | **Not** trusted | Cursor chat, Slack |
12
+ | **Not** trusted | Chat history, Slack |
13
13
 
14
14
  ## Before you code
15
15
 
@@ -20,18 +20,25 @@
20
20
  ## Workflow (human + CLI)
21
21
 
22
22
  ```bash
23
- vibeops init # once
23
+ vibeops init # once (pick Cursor / Claude Code / Codex packs)
24
24
  vibeops task add # new TASK + branch
25
- # Plan in Cursor Ask: @docs/tasks/TASK-NNN-….md
26
- # Build in Cursor Agent: same file + Scope / Acceptance Criteria
25
+ # Plan: refine TASK file with your agent (see client skills below)
26
+ # Build: implement per TASK Scope / Acceptance Criteria
27
27
  vibeops task done # close TASK, update project docs, merge
28
28
  vibeops status # briefing
29
+ vibeops llm connect # LLM for task add / task done
29
30
  ```
30
31
 
31
- ## Rules
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)
32
41
 
33
42
  - **One TASK at a time** — stay inside Scope and Acceptance Criteria.
34
43
  - **Search** before adding files; reuse existing patterns.
35
- - When the TASK is finished, **Result** and **Test Result** must be filled before `vibeops task done`.
36
-
37
- Details: `.cursor/rules/`.
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. `@docs/tasks/TASK-015-….md`).
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 and `docs/project/05-current-state.md`.
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 switch to Agent mode and implement per the TASK.
19
+ When planning is enough, tell the human to implement per the TASK (Agent mode / Codex / Claude Code as they prefer).
@@ -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