@goodtek/vibeops 0.7.22 → 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.
Files changed (144) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +51 -380
  3. package/dist/bootstrap/installer.js +42 -20
  4. package/dist/bootstrap/manifest.js +23 -3
  5. package/dist/cli.js +69 -154
  6. package/dist/commands/init.js +279 -114
  7. package/dist/commands/llm.js +177 -0
  8. package/dist/commands/status.js +181 -8
  9. package/dist/commands/task-add.js +89 -162
  10. package/dist/commands/task-done.js +112 -73
  11. package/dist/lib/config.js +77 -113
  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 +44 -1
  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/paths.js +7 -3
  24. package/dist/lib/plan-llm-openai.js +0 -63
  25. package/dist/lib/pr-create.js +66 -0
  26. package/dist/lib/resolve-task.js +34 -0
  27. package/dist/lib/task-add-llm.js +53 -233
  28. package/dist/lib/task-context.js +68 -0
  29. package/dist/lib/task-done-llm.js +155 -0
  30. package/dist/lib/task-pr-llm.js +50 -0
  31. package/dist/lib/task-push-pr.js +131 -0
  32. package/dist/lib/task-scaffold.js +39 -43
  33. package/dist/lib/task-start.js +100 -0
  34. package/dist/lib/task-summary.js +12 -239
  35. package/dist/lib/task.js +30 -24
  36. package/dist/types/config.js +0 -16
  37. package/package.json +14 -14
  38. package/templates/.cursor/rules/02-git-safety.mdc +12 -0
  39. package/templates/AGENTS.md +27 -87
  40. package/templates/clients/claude/CLAUDE.md +28 -0
  41. package/templates/clients/cursor/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  42. package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
  43. package/templates/clients/cursor/.cursor/rules/03-docs-on-done.mdc +13 -0
  44. package/templates/core/AGENTS.md +44 -0
  45. package/templates/core/docs/logs/README.md +7 -0
  46. package/templates/core/docs/project/03-architecture.md +15 -0
  47. package/templates/core/docs/project/05-current-state.md +15 -0
  48. package/templates/core/docs/project/06-decisions.md +9 -0
  49. package/templates/core/docs/tasks/TASK-000-template.md +37 -0
  50. package/templates/skills-shared/implement-task/SKILL.md +22 -0
  51. package/templates/skills-shared/plan-task/SKILL.md +19 -0
  52. package/dist/agent/loader.js +0 -71
  53. package/dist/agent/prompt.js +0 -66
  54. package/dist/commands/agent-list.js +0 -32
  55. package/dist/commands/agent-prompt.js +0 -59
  56. package/dist/commands/agent-show.js +0 -26
  57. package/dist/commands/done.js +0 -160
  58. package/dist/commands/github-init.js +0 -554
  59. package/dist/commands/github-status.js +0 -164
  60. package/dist/commands/next.js +0 -283
  61. package/dist/commands/notion-init.js +0 -764
  62. package/dist/commands/notion-sync.js +0 -405
  63. package/dist/commands/notion-test.js +0 -595
  64. package/dist/commands/plan.js +0 -146
  65. package/dist/commands/rollback.js +0 -126
  66. package/dist/commands/start.js +0 -129
  67. package/dist/commands/task-check.js +0 -155
  68. package/dist/commands/task-generate.js +0 -206
  69. package/dist/commands/task-pull.js +0 -277
  70. package/dist/commands/task-rollback.js +0 -174
  71. package/dist/commands/task-start.js +0 -102
  72. package/dist/commands/task-status.js +0 -122
  73. package/dist/lib/brief.js +0 -356
  74. package/dist/lib/cursor-task-prompt.js +0 -60
  75. package/dist/lib/done-follow-up.js +0 -308
  76. package/dist/lib/github-cli.js +0 -247
  77. package/dist/lib/guide-state.js +0 -69
  78. package/dist/lib/mvp-artifacts.js +0 -204
  79. package/dist/lib/mvp-constants.js +0 -15
  80. package/dist/lib/mvp-diff-summary.js +0 -43
  81. package/dist/lib/mvp-done-summary.js +0 -335
  82. package/dist/lib/notion-client.js +0 -459
  83. package/dist/lib/notion-discovery.js +0 -671
  84. package/dist/lib/notion-env.js +0 -140
  85. package/dist/lib/notion-mappers.js +0 -148
  86. package/dist/lib/notion-schema.js +0 -272
  87. package/dist/lib/notion-sync.js +0 -337
  88. package/dist/lib/notion-target.js +0 -247
  89. package/dist/lib/package-json.js +0 -133
  90. package/dist/lib/plan-apply-planner.js +0 -131
  91. package/dist/lib/plan-codex-pkce.js +0 -234
  92. package/dist/lib/plan-llm-models.js +0 -237
  93. package/dist/lib/plan-llm-normalize.js +0 -146
  94. package/dist/lib/plan-llm-session.js +0 -682
  95. package/dist/lib/plan-post-apply-setup.js +0 -116
  96. package/dist/lib/project-docs.js +0 -95
  97. package/dist/lib/prompt-builder.js +0 -135
  98. package/dist/lib/resolve-command-task.js +0 -40
  99. package/dist/lib/task-add-build-prompt.js +0 -39
  100. package/dist/lib/task-add-cursor.js +0 -81
  101. package/dist/lib/task-generator.js +0 -183
  102. package/dist/lib/task-merge.js +0 -220
  103. package/dist/lib/task-prompt.js +0 -23
  104. package/dist/lib/task-pull.js +0 -354
  105. package/dist/lib/task-status-brief.js +0 -132
  106. package/dist/lib/workflow-guide.js +0 -485
  107. package/dist/status/collector.js +0 -103
  108. package/dist/status/format.js +0 -181
  109. package/dist/types/brief.js +0 -126
  110. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  111. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  112. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  113. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  114. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  115. package/templates/.vibeops/agents/architect.md +0 -47
  116. package/templates/.vibeops/agents/builder.md +0 -38
  117. package/templates/.vibeops/agents/docs.md +0 -54
  118. package/templates/.vibeops/agents/orchestrator.md +0 -40
  119. package/templates/.vibeops/agents/planner.md +0 -60
  120. package/templates/.vibeops/agents/recovery.md +0 -49
  121. package/templates/.vibeops/agents/reviewer.md +0 -47
  122. package/templates/.vibeops/agents/tester.md +0 -43
  123. package/templates/.vibeops/prompts/create-plan.md +0 -33
  124. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  125. package/templates/.vibeops/prompts/implement-task.md +0 -39
  126. package/templates/.vibeops/prompts/review-task.md +0 -34
  127. package/templates/.vibeops/prompts/rollback.md +0 -32
  128. package/templates/.vibeops/prompts/start-project.md +0 -39
  129. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  130. package/templates/.vibeops/workflows/project-start.md +0 -73
  131. package/templates/.vibeops/workflows/rollback.md +0 -45
  132. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  133. package/templates/docs/logs/README.md +0 -38
  134. package/templates/docs/project/00-overview.md +0 -27
  135. package/templates/docs/project/01-requirements.md +0 -30
  136. package/templates/docs/project/02-mvp-scope.md +0 -36
  137. package/templates/docs/project/03-architecture.md +0 -34
  138. package/templates/docs/project/04-tech-stack.md +0 -29
  139. package/templates/docs/project/05-current-state.md +0 -35
  140. package/templates/docs/project/06-decisions.md +0 -20
  141. package/templates/docs/project/07-backlog.md +0 -23
  142. package/templates/docs/project/08-env.md +0 -29
  143. package/templates/docs/project/09-deployment.md +0 -28
  144. package/templates/docs/tasks/TASK-000-template.md +0 -72
@@ -1,98 +1,137 @@
1
- import { relative, resolve } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { readText } from "../lib/filesystem.js";
3
3
  import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
4
4
  import { projectPaths } from "../lib/paths.js";
5
- import { findTaskFile, hasNonEmptySection, parseTaskFilename, pickNextTask, readGitContext, readTaskFile, scanTasks, statusDisplay, updateInlineStatus, } from "../lib/task.js";
6
- function relOrAbs(root, p) {
7
- const r = relative(root, p);
8
- return r === "" ? "." : r.startsWith("..") ? p : r;
5
+ import { resolveTask, taskNotFoundMessage } from "../lib/resolve-task.js";
6
+ import { applyTaskDoneMemory, fallbackResultSections, llmCompleteTaskDone, writeTaskResultSections, } from "../lib/task-done-llm.js";
7
+ import { relPath } from "../lib/task-context.js";
8
+ import { gitAddPaths, listWorkingTreeRelPaths, partitionPathsForAutoCommit, readGitInfo, runGit, } from "../lib/git.js";
9
+ import { hasNonEmptySection, markGitContextDone, parseTaskFilename, readGitContext, readTaskFile, updateInlineStatus, } from "../lib/task.js";
10
+ import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
11
+ function commitMessageFor(taskId, title) {
12
+ const slug = title.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim() || taskId;
13
+ return `feat(${taskId.toLowerCase()}): ${slug}`;
9
14
  }
10
- function commitMessageFor(taskId, title, mvpPhase) {
11
- const trimmed = (title || taskId).trim();
12
- const slug = trimmed.replace(/^TASK-\d+\s*[·:\-]\s*/i, "").trim() || trimmed;
13
- const scope = taskId.toLowerCase();
14
- const typePrefix = typeof mvpPhase === "string" && /rollback/i.test(mvpPhase) ? "chore" : "feat";
15
- return `${typePrefix}(${scope}): ${slug}`;
15
+ async function tryCommitDirty(cwd, message, dryRun) {
16
+ const git = await readGitInfo(cwd);
17
+ if (!git.isRepo || git.dirty !== true)
18
+ return false;
19
+ const { committable, excluded } = partitionPathsForAutoCommit(await listWorkingTreeRelPaths(cwd), { unmerged: [] });
20
+ if (committable.length === 0)
21
+ return false;
22
+ if (excluded.length > 0) {
23
+ log.warn(`Skipping ${excluded.length} build artifact path(s) from auto-commit.`);
24
+ }
25
+ if (dryRun) {
26
+ log.info(dim(` would commit ${committable.length} path(s)`));
27
+ return true;
28
+ }
29
+ await gitAddPaths(cwd, committable);
30
+ await runGit(cwd, ["commit", "-q", "-m", message]);
31
+ log.ok(`Committed: ${message}`);
32
+ return true;
16
33
  }
17
- export async function taskDoneCommand(taskId, options = {}) {
34
+ export async function taskDoneCommand(taskRef, options = {}) {
18
35
  const cwd = resolve(options.cwd ?? process.cwd());
19
36
  const paths = projectPaths(cwd);
20
- const taskFile = await findTaskFile(paths.docsTasks, taskId);
21
- if (!taskFile) {
22
- log.error(`TASK not found: ${taskId} (looked in ${relOrAbs(cwd, paths.docsTasks)})`);
37
+ const dryRun = options.dryRun === true;
38
+ const resolved = await resolveTask(paths, cwd, taskRef);
39
+ if (resolved === null) {
40
+ log.error(taskNotFoundMessage(taskRef));
23
41
  process.exitCode = 1;
24
42
  return;
25
43
  }
44
+ const { taskId, taskFile } = resolved;
26
45
  const meta = await readTaskFile(taskFile);
27
46
  const body = await readText(taskFile);
28
- log.info(bold(`vibeops task done ${meta.id}`));
29
- log.info(` ${dim("file")} ${relOrAbs(cwd, taskFile)}`);
30
- log.info(` ${dim("title")} ${meta.title || dim("(no title)")}`);
31
- log.info(` ${dim("status")} ${statusDisplay(meta.status)}`);
47
+ const relFile = relPath(cwd, taskFile);
48
+ log.info(bold(`vibeops task done ${taskId}`));
49
+ log.info(` ${dim("file")} ${relFile}`);
32
50
  log.blank();
33
- const resultOk = hasNonEmptySection(body, "Result");
34
- const testResultOk = hasNonEmptySection(body, "Test Result");
35
- const docCheck = [
36
- ["Result section", resultOk],
37
- ["Test Result section", testResultOk],
38
- ];
39
- log.info(bold("Required sections"));
40
- let missing = 0;
41
- for (const [label, ok] of docCheck) {
42
- if (!ok)
43
- missing++;
44
- log.info(` ${ok ? green("✓") : yellow("·")} ${label}${ok ? "" : dim(" (empty or placeholder)")}`);
51
+ let resultOk = hasNonEmptySection(body, "Result");
52
+ let testOk = hasNonEmptySection(body, "Test Result");
53
+ if (!resultOk || !testOk) {
54
+ log.step("LLM — Result / Test Result + project memory");
55
+ if (dryRun) {
56
+ log.info(dim(" would call LLM and patch docs/project/*"));
57
+ }
58
+ else {
59
+ const patch = await llmCompleteTaskDone({
60
+ cwd,
61
+ taskId,
62
+ taskTitle: meta.title,
63
+ taskBody: body,
64
+ taskFileRel: relFile,
65
+ });
66
+ if (patch !== null) {
67
+ await writeTaskResultSections(taskFile, patch.result, patch.testResult);
68
+ log.ok(`Result / Test Result updated (${patch.provider})`);
69
+ const memory = await applyTaskDoneMemory(cwd, patch);
70
+ for (const p of memory.updated)
71
+ log.ok(`Updated ${p}`);
72
+ for (const p of memory.skipped)
73
+ log.skip(`Skipped ${p}`);
74
+ }
75
+ else {
76
+ log.warn("LLM unavailable — using git diff fallback for Result sections.");
77
+ const fb = await fallbackResultSections(cwd, taskId, taskFile);
78
+ await writeTaskResultSections(taskFile, fb.result, fb.testResult);
79
+ }
80
+ }
81
+ const body2 = dryRun ? body : await readText(taskFile);
82
+ resultOk = hasNonEmptySection(body2, "Result");
83
+ testOk = hasNonEmptySection(body2, "Test Result");
45
84
  }
85
+ log.info(bold("Sections"));
86
+ log.info(` ${resultOk ? green("✓") : yellow("·")} Result`);
87
+ log.info(` ${testOk ? green("✓") : yellow("·")} Test Result`);
46
88
  log.blank();
47
- if (missing > 0) {
48
- log.error(`${missing} required section(s) still empty. Fill Result / Test Result first, then re-run \`vibeops task done\`.`);
89
+ if (!dryRun && (!resultOk || !testOk)) {
90
+ log.error("Fill Result and Test Result in the TASK file, then rerun.");
49
91
  process.exitCode = 1;
50
92
  return;
51
93
  }
52
- const target = options.finalize === true ? "done" : "review";
53
- const targetDisplay = statusDisplay(target);
54
- if (options.dryRun === true) {
55
- log.info(bold("dry-run — would perform:"));
56
- log.info(` · update Status → ${targetDisplay} in ${relOrAbs(cwd, taskFile)}`);
57
- log.info(` · (no git commit, no Notion call)`);
58
- log.blank();
59
- }
60
- else {
61
- await updateInlineStatus(taskFile, target);
62
- log.ok(`Status → ${targetDisplay} (${relOrAbs(cwd, taskFile)})`);
63
- log.blank();
94
+ if (dryRun) {
95
+ log.info(bold("dry-run — would:"));
96
+ if (!resultOk || !testOk) {
97
+ log.info(" · LLM fill Result / Test Result + patch docs/project/*");
98
+ }
99
+ log.info(" · commit if dirty, push task branch, open MR/PR (unless --no-pr)");
100
+ log.info(" · Status → Done after push/MR succeed");
101
+ await finishTaskWithPullRequest({
102
+ cwd,
103
+ taskFile,
104
+ dryRun: true,
105
+ skipPr: options.noPr === true,
106
+ });
107
+ return;
64
108
  }
65
- const ctx = await readGitContext(taskFile);
66
109
  const parts = parseTaskFilename(taskFile);
67
- const commitMsg = commitMessageFor(parts.id, meta.title, meta.mvpPhase);
68
- log.info(bold("Suggested commit"));
69
- log.info(` ${cyan(`git add -A && git commit -m "${commitMsg}"`)}`);
70
- if (ctx !== null) {
71
- log.info(` ${dim(`after commit on ${ctx.taskBranch}: open PR or run \`git switch ${ctx.baseBranch} && git merge --no-ff ${ctx.taskBranch}\``)}`);
110
+ const commitMsg = commitMessageFor(parts.id, meta.title);
111
+ await tryCommitDirty(cwd, commitMsg, false);
112
+ const gitCtx = await readGitContext(taskFile);
113
+ if (gitCtx !== null) {
114
+ const prResult = await finishTaskWithPullRequest({
115
+ cwd,
116
+ taskFile,
117
+ skipPr: options.noPr === true,
118
+ });
119
+ if (!prResult.ok) {
120
+ log.error("TASK left In Progress — fix push/MR, then rerun task done.");
121
+ process.exitCode = 1;
122
+ return;
123
+ }
124
+ if (prResult.mergeRequestUrl) {
125
+ log.info(` ${dim("MR/PR")} ${prResult.mergeRequestUrl}`);
126
+ log.info(dim("Merge on GitHub/GitLab — CI deploys after merge."));
127
+ }
72
128
  }
73
129
  else {
74
- log.info(` ${dim("(no Git Context section — skipping merge guidance)")}`);
130
+ log.info(dim("No Git Context — push/MR skipped."));
75
131
  }
132
+ await updateInlineStatus(taskFile, "done");
133
+ await markGitContextDone(taskFile);
134
+ log.ok("Status → Done");
76
135
  log.blank();
77
- log.info(bold("Notion"));
78
- log.info(` ${dim("TODO: run `vibeops notion sync` to push this TASK metadata.")}`);
79
- log.blank();
80
- const all = await scanTasks(paths.docsTasks);
81
- const next = pickNextTask(all.filter((t) => t.id.toUpperCase() !== meta.id.toUpperCase()));
82
- log.info(bold("Next TASK candidate"));
83
- if (next) {
84
- log.info(` → ${cyan(next.id)} — ${next.title || dim("(no title)")} ${dim(`[${statusDisplay(next.status)}]`)}`);
85
- log.info(` file: ${dim(relOrAbs(cwd, next.filePath))}`);
86
- log.info(` start: ${dim(`vibeops task start ${next.id}`)}`);
87
- }
88
- else {
89
- log.info(` ${dim("(no remaining planned / in-progress / review TASK)")}`);
90
- }
91
- log.blank();
92
- if (target === "review") {
93
- log.info(`Status moved to ${cyan("Review")}. After human or Reviewer Agent review, finalize with ${cyan(`vibeops task done ${meta.id} --finalize`)}.`);
94
- }
95
- else {
96
- log.ok(`${meta.id} finalized → Done.`);
97
- }
136
+ log.info(`Next: ${cyan("vibeops task add")} or ${cyan("vibeops status")}`);
98
137
  }
@@ -1,39 +1,56 @@
1
1
  import { join } from "node:path";
2
- import { readTextOrNull, writeText } from "./filesystem.js";
2
+ import { pathExists, readTextOrNull, writeText } from "./filesystem.js";
3
3
  import { VIBEOPS_CONFIG_FILE } from "./paths.js";
4
4
  import { VERSION } from "../version.js";
5
- import { DEFAULT_GITHUB_CONFIG, VIBEOPS_CONFIG_SCHEMA_VERSION, } from "../types/config.js";
6
- function parseNotionSection(raw) {
7
- if (raw === null || typeof raw !== "object")
5
+ import { VIBEOPS_CONFIG_SCHEMA_VERSION, } from "../types/config.js";
6
+ import { isVibeopsClientId } from "./init-clients.js";
7
+ function parseClientsBlock(raw) {
8
+ if (!Array.isArray(raw))
8
9
  return undefined;
9
- const r = raw;
10
- const out = {
11
- enabled: typeof r.enabled === "boolean" ? r.enabled : false,
12
- projectsTargetId: typeof r.projectsTargetId === "string" ? r.projectsTargetId : "",
13
- tasksTargetId: typeof r.tasksTargetId === "string" ? r.tasksTargetId : "",
14
- projectsDatabaseId: typeof r.projectsDatabaseId === "string" ? r.projectsDatabaseId : "",
15
- tasksDatabaseId: typeof r.tasksDatabaseId === "string" ? r.tasksDatabaseId : "",
16
- };
17
- return out;
10
+ const out = [];
11
+ for (const item of raw) {
12
+ if (typeof item === "string" && isVibeopsClientId(item) && !out.includes(item)) {
13
+ out.push(item);
14
+ }
15
+ }
16
+ return out.length > 0 ? out : undefined;
18
17
  }
19
- function parseGithubVisibility(raw) {
20
- if (raw === "public" || raw === "private")
21
- return raw;
22
- return "";
18
+ export function getClientsFromConfig(config) {
19
+ if (config?.clients && config.clients.length > 0)
20
+ return [...config.clients];
21
+ return ["cursor"];
23
22
  }
24
- function parseGithubSection(raw) {
25
- if (raw === null || typeof raw !== "object")
23
+ export async function isVibeopsProject(root) {
24
+ if ((await readConfig(root)) !== null)
25
+ return true;
26
+ return pathExists(join(root, VIBEOPS_CONFIG_FILE));
27
+ }
28
+ function parseGitBlock(raw) {
29
+ if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) {
26
30
  return undefined;
27
- const r = raw;
28
- return {
29
- enabled: typeof r.enabled === "boolean" ? r.enabled : false,
30
- mode: r.mode === "gh-cli" ? "gh-cli" : "gh-cli",
31
- owner: typeof r.owner === "string" ? r.owner : "",
32
- repo: typeof r.repo === "string" ? r.repo : "",
33
- remote: typeof r.remote === "string" && r.remote.length > 0 ? r.remote : "origin",
34
- visibility: parseGithubVisibility(r.visibility),
35
- url: typeof r.url === "string" ? r.url : "",
36
- };
31
+ }
32
+ const o = raw;
33
+ const remote = typeof o.remote === "string" && o.remote.length > 0 ? o.remote : "origin";
34
+ const host = o.host === "github" || o.host === "gitlab" ? o.host : undefined;
35
+ const integrationBranch = typeof o.integrationBranch === "string" && o.integrationBranch.length > 0
36
+ ? o.integrationBranch
37
+ : undefined;
38
+ const productionBranch = typeof o.productionBranch === "string" && o.productionBranch.length > 0
39
+ ? o.productionBranch
40
+ : undefined;
41
+ if (!host || !integrationBranch || !productionBranch)
42
+ return undefined;
43
+ return { remote, host, integrationBranch, productionBranch };
44
+ }
45
+ function parseLlmBlock(raw) {
46
+ if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) {
47
+ return undefined;
48
+ }
49
+ const p = raw.provider;
50
+ if (p === "auto" || p === "codex-oauth" || p === "cursor-agent" || p === "openai") {
51
+ return { provider: p };
52
+ }
53
+ return undefined;
37
54
  }
38
55
  export async function readConfig(root) {
39
56
  const text = await readTextOrNull(join(root, VIBEOPS_CONFIG_FILE));
@@ -45,19 +62,18 @@ export async function readConfig(root) {
45
62
  typeof parsed.vibeopsVersion === "string" &&
46
63
  typeof parsed.createdAt === "string" &&
47
64
  parsed.schemaVersion === VIBEOPS_CONFIG_SCHEMA_VERSION) {
48
- const notion = parseNotionSection(parsed.notion);
49
- const github = parseGithubSection(parsed.github);
50
- const config = {
65
+ const llm = parseLlmBlock(parsed.llm);
66
+ const git = parseGitBlock(parsed.git);
67
+ const clients = parseClientsBlock(parsed.clients) ?? ["cursor"];
68
+ return {
51
69
  name: parsed.name,
52
70
  vibeopsVersion: parsed.vibeopsVersion,
53
71
  schemaVersion: VIBEOPS_CONFIG_SCHEMA_VERSION,
54
72
  createdAt: parsed.createdAt,
73
+ clients,
74
+ ...(git ? { git } : {}),
75
+ ...(llm ? { llm } : {}),
55
76
  };
56
- if (notion)
57
- config.notion = notion;
58
- if (github)
59
- config.github = github;
60
- return config;
61
77
  }
62
78
  return null;
63
79
  }
@@ -65,12 +81,15 @@ export async function readConfig(root) {
65
81
  return null;
66
82
  }
67
83
  }
68
- export function buildConfig(name, nowIso) {
84
+ export function buildConfig(name, clients, git, existing, nowIso) {
69
85
  return {
70
86
  name,
71
87
  vibeopsVersion: VERSION,
72
88
  schemaVersion: VIBEOPS_CONFIG_SCHEMA_VERSION,
73
- createdAt: nowIso ?? new Date().toISOString(),
89
+ createdAt: existing?.createdAt ?? nowIso ?? new Date().toISOString(),
90
+ clients,
91
+ git,
92
+ llm: existing?.llm ?? { provider: "auto" },
74
93
  };
75
94
  }
76
95
  export function configToJson(config) {
@@ -79,80 +98,25 @@ export function configToJson(config) {
79
98
  export async function writeConfig(root, config) {
80
99
  await writeText(join(root, VIBEOPS_CONFIG_FILE), configToJson(config));
81
100
  }
82
- /** Merge a partial notion section into an existing config without touching other fields. */
83
- export function mergeNotionConfig(base, patch) {
84
- const current = base.notion ?? {
85
- enabled: false,
86
- projectsTargetId: "",
87
- tasksTargetId: "",
88
- projectsDatabaseId: "",
89
- tasksDatabaseId: "",
90
- };
91
- const next = {
92
- enabled: patch.enabled ?? current.enabled,
93
- projectsTargetId: patch.projectsTargetId !== undefined && patch.projectsTargetId.length > 0
94
- ? patch.projectsTargetId
95
- : current.projectsTargetId,
96
- tasksTargetId: patch.tasksTargetId !== undefined && patch.tasksTargetId.length > 0
97
- ? patch.tasksTargetId
98
- : current.tasksTargetId,
99
- projectsDatabaseId: patch.projectsDatabaseId !== undefined && patch.projectsDatabaseId.length > 0
100
- ? patch.projectsDatabaseId
101
- : current.projectsDatabaseId,
102
- tasksDatabaseId: patch.tasksDatabaseId !== undefined && patch.tasksDatabaseId.length > 0
103
- ? patch.tasksDatabaseId
104
- : current.tasksDatabaseId,
105
- };
106
- const changed = base.notion === undefined ||
107
- next.enabled !== current.enabled ||
108
- next.projectsTargetId !== current.projectsTargetId ||
109
- next.tasksTargetId !== current.tasksTargetId ||
110
- next.projectsDatabaseId !== current.projectsDatabaseId ||
111
- next.tasksDatabaseId !== current.tasksDatabaseId;
112
- return { merged: { ...base, notion: next }, changed };
113
- }
114
- /** Preferred Projects target id for API calls: data_source target first, legacy DB fallback. */
115
- export function notionProjectsTargetId(notion) {
116
- return notion.projectsTargetId.length > 0
117
- ? notion.projectsTargetId
118
- : notion.projectsDatabaseId;
119
- }
120
- /** Preferred Tasks target id for API calls: data_source target first, legacy DB fallback. */
121
- export function notionTasksTargetId(notion) {
122
- return notion.tasksTargetId.length > 0 ? notion.tasksTargetId : notion.tasksDatabaseId;
123
- }
124
101
  /**
125
- * Merge a partial github section into an existing config without touching
126
- * other fields (notion, name, etc.). Empty strings in the patch are treated
127
- * as "no value provided" — the existing value is kept. Pass through
128
- * `enabled` / `mode` always (booleans / enums always overwrite).
102
+ * Merge `llm.provider` into existing `.vibeops.json` (preserves other keys).
129
103
  */
130
- export function mergeGithubConfig(base, patch) {
131
- const current = base.github ?? DEFAULT_GITHUB_CONFIG;
132
- const next = {
133
- enabled: patch.enabled ?? current.enabled,
134
- mode: patch.mode ?? current.mode,
135
- owner: patch.owner !== undefined && patch.owner.length > 0
136
- ? patch.owner
137
- : current.owner,
138
- repo: patch.repo !== undefined && patch.repo.length > 0
139
- ? patch.repo
140
- : current.repo,
141
- remote: patch.remote !== undefined && patch.remote.length > 0
142
- ? patch.remote
143
- : current.remote,
144
- visibility: patch.visibility !== undefined && patch.visibility.length > 0
145
- ? patch.visibility
146
- : current.visibility,
147
- url: patch.url !== undefined && patch.url.length > 0 ? patch.url : current.url,
148
- };
149
- const changed = base.github === undefined ||
150
- next.enabled !== current.enabled ||
151
- next.mode !== current.mode ||
152
- next.owner !== current.owner ||
153
- next.repo !== current.repo ||
154
- next.remote !== current.remote ||
155
- next.visibility !== current.visibility ||
156
- next.url !== current.url;
157
- return { merged: { ...base, github: next }, changed };
104
+ export async function setLlmProviderPreference(root, provider) {
105
+ const path = join(root, VIBEOPS_CONFIG_FILE);
106
+ const text = await readTextOrNull(path);
107
+ if (text === null) {
108
+ throw new Error(`${VIBEOPS_CONFIG_FILE} not found — run vibeops init first.`);
109
+ }
110
+ let data;
111
+ try {
112
+ data = JSON.parse(text);
113
+ }
114
+ catch {
115
+ throw new Error(`${VIBEOPS_CONFIG_FILE} is not valid JSON.`);
116
+ }
117
+ const prevLlm = typeof data.llm === "object" && data.llm !== null && !Array.isArray(data.llm)
118
+ ? data.llm
119
+ : {};
120
+ data.llm = { ...prevLlm, provider };
121
+ await writeText(path, `${JSON.stringify(data, null, 2)}\n`);
158
122
  }
@@ -0,0 +1,32 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { VIBEOPS_ENV_FILE } from "./paths.js";
4
+ /**
5
+ * Loads `KEY=value` pairs from `.vibeops.env` without overwriting existing `process.env`.
6
+ */
7
+ export async function loadVibeopsEnv(cwd) {
8
+ const path = join(resolve(cwd), VIBEOPS_ENV_FILE);
9
+ try {
10
+ const text = await readFile(path, "utf-8");
11
+ for (const line of text.split("\n")) {
12
+ const t = line.trim();
13
+ if (t.length === 0 || t.startsWith("#"))
14
+ continue;
15
+ const eq = t.indexOf("=");
16
+ if (eq <= 0)
17
+ continue;
18
+ const key = t.slice(0, eq).trim();
19
+ let val = t.slice(eq + 1).trim();
20
+ if ((val.startsWith('"') && val.endsWith('"')) ||
21
+ (val.startsWith("'") && val.endsWith("'"))) {
22
+ val = val.slice(1, -1);
23
+ }
24
+ if (key.length > 0 && process.env[key] === undefined) {
25
+ process.env[key] = val;
26
+ }
27
+ }
28
+ }
29
+ catch {
30
+ /* missing or unreadable — ok */
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ import { readConfig } from "./config.js";
2
+ export class GitConfigError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "GitConfigError";
6
+ }
7
+ }
8
+ /** Load `.vibeops.json` git block or throw with re-init guidance. */
9
+ export async function requireGitConfig(cwd) {
10
+ const config = await readConfig(cwd);
11
+ if (!config) {
12
+ throw new GitConfigError("No .vibeops.json found. Run vibeops init in this project first.");
13
+ }
14
+ if (!config.git?.integrationBranch || !config.git?.productionBranch) {
15
+ throw new GitConfigError("Missing git policy in .vibeops.json. Re-run vibeops init to set integration and production branches.");
16
+ }
17
+ if (!config.git.host || !config.git.remote) {
18
+ throw new GitConfigError("Incomplete git config in .vibeops.json. Re-run vibeops init.");
19
+ }
20
+ return config.git;
21
+ }
@@ -0,0 +1,12 @@
1
+ /** Infer Git host from a remote URL. */
2
+ export function detectGitHost(remoteUrl) {
3
+ const u = remoteUrl.trim().toLowerCase();
4
+ if (u.includes("github.com"))
5
+ return "github";
6
+ if (u.includes("gitlab"))
7
+ return "gitlab";
8
+ return null;
9
+ }
10
+ export function mergeRequestLabel(host) {
11
+ return host === "gitlab" ? "Merge request" : "Pull request";
12
+ }
@@ -0,0 +1,67 @@
1
+ import { select } from "@inquirer/prompts";
2
+ import { askInput } from "./inquirer-helpers.js";
3
+ export const GIT_POLICY_PRESETS = [
4
+ {
5
+ id: "gitflow",
6
+ label: "GitFlow lite — develop (integration) + main (production)",
7
+ integrationBranch: "develop",
8
+ productionBranch: "main",
9
+ },
10
+ {
11
+ id: "trunk",
12
+ label: "Trunk — main only (integration and production = main)",
13
+ integrationBranch: "main",
14
+ productionBranch: "main",
15
+ },
16
+ ];
17
+ export function parseGitPolicyArg(raw) {
18
+ if (raw === undefined || raw.trim().length === 0)
19
+ return null;
20
+ const t = raw.trim().toLowerCase();
21
+ if (t === "gitflow" || t === "develop-main")
22
+ return "gitflow";
23
+ if (t === "trunk" || t === "main")
24
+ return "trunk";
25
+ if (t === "custom")
26
+ return "custom";
27
+ return null;
28
+ }
29
+ export function resolvePreset(id) {
30
+ return GIT_POLICY_PRESETS.find((p) => p.id === id) ?? null;
31
+ }
32
+ export async function askGitPolicy() {
33
+ const preset = await select({
34
+ message: "Branch policy",
35
+ choices: [
36
+ ...GIT_POLICY_PRESETS.map((p) => ({ name: p.label, value: p.id })),
37
+ { name: "Custom branch names", value: "custom" },
38
+ ],
39
+ loop: false,
40
+ });
41
+ if (preset !== "custom") {
42
+ const p = resolvePreset(preset);
43
+ return {
44
+ integrationBranch: p.integrationBranch,
45
+ productionBranch: p.productionBranch,
46
+ };
47
+ }
48
+ const integrationBranch = await askInput({
49
+ message: "Integration branch (task branches start here; MR/PR target)",
50
+ nonInteractive: false,
51
+ default: "develop",
52
+ required: true,
53
+ });
54
+ const productionBranch = await askInput({
55
+ message: "Production branch (releases / deploy; merge manually)",
56
+ nonInteractive: false,
57
+ default: "main",
58
+ required: true,
59
+ });
60
+ return { integrationBranch, productionBranch };
61
+ }
62
+ export function formatGitPolicySummary(git) {
63
+ if (git.integrationBranch === git.productionBranch) {
64
+ return `${git.integrationBranch} (integration + production)`;
65
+ }
66
+ return `integration=${git.integrationBranch}, production=${git.productionBranch}`;
67
+ }