@goodtek/vibeops 0.2.0 → 0.3.0
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 +5 -4
- package/dist/cli.js +32 -5
- package/dist/commands/plan.js +242 -13
- package/dist/commands/task-start.js +16 -4
- package/dist/lib/brief.js +11 -4
- package/dist/lib/git.js +42 -0
- package/dist/lib/plan-apply-planner.js +131 -0
- package/dist/lib/plan-codex-auth.js +183 -0
- package/dist/lib/plan-codex-pkce.js +234 -0
- package/dist/lib/plan-codex-responses.js +228 -0
- package/dist/lib/plan-llm-cursor-agent.js +59 -0
- package/dist/lib/plan-llm-detect.js +74 -0
- package/dist/lib/plan-llm-models.js +237 -0
- package/dist/lib/plan-llm-normalize.js +146 -0
- package/dist/lib/plan-llm-openai.js +123 -0
- package/dist/lib/plan-llm-session.js +681 -0
- package/dist/lib/plan-llm-types.js +1 -0
- package/dist/lib/plan-post-apply-setup.js +116 -0
- package/dist/lib/prompt-builder.js +11 -1
- package/package.json +1 -1
- package/templates/.cursor/rules/03-git-safety.mdc +1 -1
- package/templates/.vibeops/workflows/task-lifecycle.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to VibeOps are documented here.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.3.0 - 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`vibeops plan` requires `vibeops init`**: exits early if `.vibeops.json` is missing so partial `.vibeops/` trees are not created outside a full VibeOps project.
|
|
12
|
+
- **Interactive LLM planning** (default): OpenAI API key, **Codex (ChatGPT OAuth)** via `~/.codex/auth.json`, or **Cursor Agent CLI**; always lists all three providers; live model catalogs (`/v1/models`, Codex `/models` with `client_version`, `agent models`); **`--model`** skips the picker. Codex uses ChatGPT-account models (default **`gpt-5.4`**); **`VIBEOPS_CODEX_CLIENT_VERSION`** / `codex --version` / bundled default for `client_version`.
|
|
13
|
+
- **Planning dialogue UX**: terminal **language picker** after model; **one question per turn**; **wrap up early** (`wrap` / `enough` or picker row) → `confirm` only; **go back** on pickers / `back` for text; **rough progress + ETA** line per question; **`confirm` then terminal Yes** before **`done`** ProjectBrief.
|
|
14
|
+
- **`--apply-planner`**: second LLM pass from `plan-prompt.md`, parses `<!-- file: docs/... -->` fences, writes **`docs/project/*`** and **`docs/tasks/*`**; interactive **commit → optional `github init` before push → push → Notion sync** prompts; **`--apply-dry-run`**, **`--no-git-commit`**, **`--push`**, **`--no-notion-sync`**; optional **`git init`**, **`notion init`**, same flow as **`vibeops github init`** when setup is missing.
|
|
15
|
+
- **LLM protocol / system prompt**: product-first pacing; session rule for wrap-up → `confirm` only.
|
|
16
|
+
- **`vibeops task start`**: allows a dirty tree when changes are limited to governance doc paths (`docs/tasks/`, `docs/project/`, `docs/logs/`, `.vibeops/state/`).
|
|
17
|
+
|
|
5
18
|
## 0.2.0 - 2026-05-12
|
|
6
19
|
|
|
7
20
|
Public release polish.
|
package/README.md
CHANGED
|
@@ -70,7 +70,8 @@ vibeops init
|
|
|
70
70
|
# Non-interactive Git bootstrap:
|
|
71
71
|
vibeops init --git --initial-commit
|
|
72
72
|
|
|
73
|
-
# 2.
|
|
73
|
+
# 2. LLM planning session (OpenAI API key, Codex `codex login` / ~/.codex/auth.json, or Cursor `agent` CLI) → brief + planning prompt.
|
|
74
|
+
# Use `vibeops plan --legacy-wizard` for the original 20-question flow.
|
|
74
75
|
vibeops plan
|
|
75
76
|
|
|
76
77
|
# 3. Generate a TASK prompt or scaffold TASK markdown.
|
|
@@ -126,7 +127,7 @@ vibeops
|
|
|
126
127
|
│ [--git|--no-git] [--initial-commit|--no-initial-commit]
|
|
127
128
|
│ [--default-branch <name>] [--commit-message <message>]
|
|
128
129
|
├─ status [--json] [--cwd <path>]
|
|
129
|
-
├─ plan [--idea <text>] [--from <path>] [--output <path>] [--non-interactive] [--cwd <path>]
|
|
130
|
+
├─ plan [--idea <text>] [--from <path>] [--output <path>] [--non-interactive] [--legacy-wizard] [--provider <id>] [--cwd <path>]
|
|
130
131
|
├─ agent
|
|
131
132
|
│ ├─ list [--json] [--cwd <path>]
|
|
132
133
|
│ ├─ show <name> [--raw] [--cwd <path>]
|
|
@@ -159,7 +160,7 @@ Run any command with `--help` for the option details.
|
|
|
159
160
|
|
|
160
161
|
### Interactive Planner
|
|
161
162
|
|
|
162
|
-
`vibeops plan` asks
|
|
163
|
+
`vibeops plan` runs only inside a **VibeOps project** (expects **`.vibeops.json`** from `vibeops init`). It writes `.vibeops/brief/project-brief.md` and `.vibeops/generated/plan-prompt.md`; with **`--apply-planner`** it also writes `docs/project/*` and `docs/tasks/*` via the LLM. The command uses **OpenAI** (`OPENAI_API_KEY`), **Codex (ChatGPT OAuth)** via tokens from `codex login` (`~/.codex/auth.json`, same OAuth client as Hermes/OpenClaw `openai-codex`), or the **Cursor Agent CLI** (`agent login`). With a TTY you **always pick** among those three (each line shows ✓ or “needs setup”; Codex can start browser OAuth from that menu). Pass `--provider` to skip the menu when that path is already ready. Then pick a **model** from the provider catalog (or `--model`). Next you pick the **planning dialogue language** (preset list + Other); the LLM asks **one question per turn** in that language. Each question header shows a **rough ASCII progress line** and **ETA hint** (assumes up to ~14 steps; not a guarantee). **Multi**-choice suggestions use **checkboxes** (Space, Other, **Wrap up — draft summary now** when allowed, then **Go back** last when you can undo); **single**-choice uses **arrow select** with **Other**, wrap-up, and **Go back** last (no extra Continue step). **Text** answers: type `back` alone to go back when allowed, or `wrap` / `enough` to stop questions and ask for a draft summary from the thread so far. When the model is ready, it shows a **readable markdown summary**; you must **confirm** before the `ProjectBrief` is written. After that, **`--apply-planner`** runs the planner pass via the same provider: fills `docs/project/*` and initial `docs/tasks/*`. With a **TTY** and without **`--non-interactive`**, you are then asked **in order**: create a **git commit** (default Yes; if Git is not initialized yet, you can run **`git init`** + branch **`main`** from the same prompts), **git push** (default No; use **`--push`** to skip the question and push after a successful commit; after you choose push, you can run **`vibeops github init`** to connect or create a GitHub repo / set **`origin`**, same as the standalone command), and **Notion sync** when the brief enables it (default Yes; **`--no-notion-sync`** skips; if Notion is not configured, you can run **`vibeops notion init`** from the same flow, then sync retries once ready). In **`--non-interactive`** mode, commit runs automatically (unless **`--no-git-commit`**), push only with **`--push`**, and Notion runs when the brief enables it (unless **`--no-notion-sync`**). **`--apply-dry-run`** asks the model which files it would emit without writing. Codex defaults to ChatGPT-account models (`gpt-5.4`; override with `VIBEOPS_CODEX_MODEL`). Use `--legacy-wizard` for the fixed 20-question flow. `--non-interactive` and `--from` never call an LLM for the brief unless you add `--apply-planner` with `--provider` (and optional `--model`) for the apply step.
|
|
163
164
|
|
|
164
165
|
### Task Generator
|
|
165
166
|
|
|
@@ -167,7 +168,7 @@ Run any command with `--help` for the option details.
|
|
|
167
168
|
|
|
168
169
|
### Git Task Lifecycle
|
|
169
170
|
|
|
170
|
-
`task start`, `task prompt`, `task check`, and `task done` keep one TASK moving through `Planned → In Progress → Review → Done`. `task done` defaults to `Review`; use `--finalize` only after human review.
|
|
171
|
+
`task start`, `task prompt`, `task check`, and `task done` keep one TASK moving through `Planned → In Progress → Review → Done`. `task done` defaults to `Review`; use `--finalize` only after human review. `task start` still expects a clean tree for application code, but if the only uncommitted changes live under `docs/tasks/`, `docs/project/`, `docs/logs/`, or `.vibeops/state/`, it warns and continues so a prior doc-only finalize does not block the next TASK.
|
|
171
172
|
|
|
172
173
|
### Rollback Safety
|
|
173
174
|
|
package/dist/cli.js
CHANGED
|
@@ -51,14 +51,41 @@ program
|
|
|
51
51
|
});
|
|
52
52
|
program
|
|
53
53
|
.command("plan")
|
|
54
|
-
.description("
|
|
54
|
+
.description("Requires `vibeops init`: LLM planning → ProjectBrief + planning prompt; optional --apply-planner runs the planner via the same LLM (docs + TASKs, commit, push, Notion). Use --legacy-wizard for the fixed 20-question flow")
|
|
55
55
|
.option("--idea <text>", "One-line idea default (use `Name: idea` to extract the project name)")
|
|
56
56
|
.option("--from <path>", "Read an existing brief markdown and regenerate the prompt")
|
|
57
57
|
.option("--output <path>", "Output path for the Cursor planning prompt (default `.vibeops/generated/plan-prompt.md`)")
|
|
58
|
-
.option("--non-interactive", "Skip prompts and use the supplied values plus safe placeholders")
|
|
58
|
+
.option("--non-interactive", "Skip prompts and use the supplied values plus safe placeholders (no LLM)")
|
|
59
|
+
.option("--legacy-wizard", "Use the original 20-question wizard instead of an LLM planning session")
|
|
60
|
+
.option("--provider <id>", "LLM provider when several are available: openai | codex-oauth | cursor-agent")
|
|
61
|
+
.option("--model <id>", "LLM model id for the selected provider (skips interactive model list; same as setting VIBEOPS_*_MODEL for that path)")
|
|
59
62
|
.option("--cwd <path>", "Run against a different directory")
|
|
63
|
+
.option("--apply-planner", "After writing the brief + prompt: call the same LLM provider to emit docs/project + docs/tasks; then (TTY) ask commit → push → Notion in order, or (--non-interactive) commit unless --no-git-commit, push only with --push, Notion unless --no-notion-sync")
|
|
64
|
+
.option("--apply-dry-run", "With --apply-planner (or alone): call the LLM and list files that would be written; no writes, git, or Notion")
|
|
65
|
+
.option("--no-git-commit", "With --apply-planner: skip git commit after writes")
|
|
66
|
+
.option("--push", "With --apply-planner: run git push after commit (TTY: optional `vibeops github init` before push; skips the push Yes/No prompt)")
|
|
67
|
+
.option("--no-notion-sync", "With --apply-planner: skip Notion sync even when the brief enables it")
|
|
60
68
|
.action(async (options) => {
|
|
61
|
-
|
|
69
|
+
const provider = options.provider === "openai" ||
|
|
70
|
+
options.provider === "codex-oauth" ||
|
|
71
|
+
options.provider === "cursor-agent"
|
|
72
|
+
? options.provider
|
|
73
|
+
: undefined;
|
|
74
|
+
await planCommand({
|
|
75
|
+
idea: options.idea,
|
|
76
|
+
from: options.from,
|
|
77
|
+
output: options.output,
|
|
78
|
+
nonInteractive: options.nonInteractive,
|
|
79
|
+
legacyWizard: options.legacyWizard === true,
|
|
80
|
+
provider,
|
|
81
|
+
llmModel: typeof options.model === "string" ? options.model : undefined,
|
|
82
|
+
cwd: options.cwd,
|
|
83
|
+
applyPlanner: options.applyPlanner === true,
|
|
84
|
+
applyPlannerDryRun: options.applyDryRun === true,
|
|
85
|
+
noGitCommit: options.noGitCommit === true,
|
|
86
|
+
gitPush: options.push === true,
|
|
87
|
+
noNotionSync: options.noNotionSync === true,
|
|
88
|
+
});
|
|
62
89
|
});
|
|
63
90
|
const agent = program
|
|
64
91
|
.command("agent")
|
|
@@ -105,9 +132,9 @@ task
|
|
|
105
132
|
});
|
|
106
133
|
task
|
|
107
134
|
.command("start <taskId>")
|
|
108
|
-
.description("Confirm a clean working tree, create the task branch, record Status/Git context, and print a Builder prompt")
|
|
135
|
+
.description("Confirm a clean working tree (or docs-only governance changes), create the task branch, record Status/Git context, and print a Builder prompt")
|
|
109
136
|
.option("--dry-run", "Print the plan without touching files or Git")
|
|
110
|
-
.option("--allow-dirty", "Proceed even if the Git working tree is dirty")
|
|
137
|
+
.option("--allow-dirty", "Proceed even if the Git working tree is dirty (including non-doc changes)")
|
|
111
138
|
.option("--agent <name>", "Agent to build the prompt with (default `builder`)")
|
|
112
139
|
.option("--cwd <path>", "Run against a different directory")
|
|
113
140
|
.action(async (taskId, options) => {
|
package/dist/commands/plan.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
2
2
|
import { briefToMarkdown, findMissingRequired, gatherBrief, parseBriefFromMarkdown, parseIdea, } from "../lib/brief.js";
|
|
3
|
+
import { askYesNo } from "../lib/inquirer-helpers.js";
|
|
3
4
|
import { pathExists, readText, writeText } from "../lib/filesystem.js";
|
|
4
5
|
import { bold, cyan, dim, log, yellow } from "../lib/logger.js";
|
|
6
|
+
import { applyPlannerMarkdownFlow, commitPlannerGit, pushPlannerGit } from "../lib/plan-apply-planner.js";
|
|
7
|
+
import { gatherBriefViaLlm, pickPlanDialogueLocale, pickPlanLlmProvider } from "../lib/plan-llm-session.js";
|
|
8
|
+
import { pickPlanExecutionModel } from "../lib/plan-llm-models.js";
|
|
9
|
+
import { ensureGitForPlannerCommit, offerGithubInitBeforePush, runNotionSyncWithOptionalInit } from "../lib/plan-post-apply-setup.js";
|
|
5
10
|
import { buildPlanPrompt } from "../lib/prompt-builder.js";
|
|
11
|
+
import { projectPaths } from "../lib/paths.js";
|
|
6
12
|
const DEFAULT_BRIEF_REL = ".vibeops/brief/project-brief.md";
|
|
7
13
|
const DEFAULT_PROMPT_REL = ".vibeops/generated/plan-prompt.md";
|
|
8
14
|
function toAbsolute(root, candidate) {
|
|
@@ -15,6 +21,13 @@ function relDisplay(root, abs) {
|
|
|
15
21
|
export async function planCommand(options) {
|
|
16
22
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
17
23
|
const nonInteractive = options.nonInteractive === true;
|
|
24
|
+
const paths = projectPaths(cwd);
|
|
25
|
+
if (!(await pathExists(paths.config))) {
|
|
26
|
+
log.error(`Not a VibeOps project: ${cyan(".vibeops.json")} was not found under this directory.`);
|
|
27
|
+
log.info(` ${dim("Run")} ${cyan("vibeops init")} ${dim("here first, then run")} ${cyan("vibeops plan")} ${dim("again.")}`);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
18
31
|
const briefAbs = join(cwd, DEFAULT_BRIEF_REL);
|
|
19
32
|
const promptAbs = typeof options.output === "string" ? toAbsolute(cwd, options.output) : join(cwd, DEFAULT_PROMPT_REL);
|
|
20
33
|
log.info(bold(`vibeops plan`));
|
|
@@ -23,10 +36,15 @@ export async function planCommand(options) {
|
|
|
23
36
|
log.info(dim(` from: ${options.from}`));
|
|
24
37
|
if (options.idea)
|
|
25
38
|
log.info(dim(` idea: ${options.idea}`));
|
|
39
|
+
if (typeof options.llmModel === "string" && options.llmModel.trim().length > 0) {
|
|
40
|
+
log.info(dim(` model: ${options.llmModel.trim()}`));
|
|
41
|
+
}
|
|
26
42
|
if (nonInteractive)
|
|
27
43
|
log.info(dim(` mode: non-interactive`));
|
|
28
44
|
log.blank();
|
|
29
45
|
let bundle;
|
|
46
|
+
let llmProviderForApply;
|
|
47
|
+
let llmModelForApply;
|
|
30
48
|
if (typeof options.from === "string" && options.from.length > 0) {
|
|
31
49
|
bundle = await loadFromFile({
|
|
32
50
|
cwd,
|
|
@@ -41,15 +59,64 @@ export async function planCommand(options) {
|
|
|
41
59
|
process.exitCode = 1;
|
|
42
60
|
return;
|
|
43
61
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
62
|
+
if (nonInteractive) {
|
|
63
|
+
log.step("non-interactive: build the ProjectBrief from flag values + safe placeholders");
|
|
64
|
+
log.blank();
|
|
65
|
+
bundle = await gatherBrief({
|
|
66
|
+
cwd,
|
|
67
|
+
idea: options.idea,
|
|
68
|
+
nonInteractive: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (options.legacyWizard === true) {
|
|
72
|
+
log.step("legacy wizard: build the ProjectBrief from 20 fixed questions (arrow keys · Space · Enter)");
|
|
73
|
+
log.blank();
|
|
74
|
+
bundle = await gatherBrief({
|
|
75
|
+
cwd,
|
|
76
|
+
idea: options.idea,
|
|
77
|
+
nonInteractive: false,
|
|
78
|
+
legacyWizard: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
log.step("LLM planning: verify provider, then interactive discovery + structured brief");
|
|
83
|
+
log.blank();
|
|
84
|
+
const interactiveLogin = process.stdin.isTTY === true;
|
|
85
|
+
const provider = await pickPlanLlmProvider(cwd, options.provider, { interactiveLogin });
|
|
86
|
+
if (provider === null) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const forcedModel = typeof options.llmModel === "string" && options.llmModel.trim().length > 0
|
|
90
|
+
? options.llmModel.trim()
|
|
91
|
+
: undefined;
|
|
92
|
+
const pickedModel = forcedModel ??
|
|
93
|
+
(await pickPlanExecutionModel({
|
|
94
|
+
provider,
|
|
95
|
+
cwd,
|
|
96
|
+
interactive: interactiveLogin,
|
|
97
|
+
}));
|
|
98
|
+
log.blank();
|
|
99
|
+
const planningLocale = await pickPlanDialogueLocale();
|
|
100
|
+
try {
|
|
101
|
+
bundle = await gatherBriefViaLlm({
|
|
102
|
+
cwd,
|
|
103
|
+
idea: options.idea,
|
|
104
|
+
provider,
|
|
105
|
+
allowCodexBrowserLogin: interactiveLogin,
|
|
106
|
+
llmModelOverride: pickedModel,
|
|
107
|
+
planningLocale,
|
|
108
|
+
});
|
|
109
|
+
llmProviderForApply = provider;
|
|
110
|
+
llmModelForApply =
|
|
111
|
+
typeof pickedModel === "string" && pickedModel.trim().length > 0 ? pickedModel.trim() : undefined;
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
115
|
+
log.error(`LLM planning failed: ${msg}`);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
53
120
|
}
|
|
54
121
|
const briefMd = briefToMarkdown(bundle.brief, bundle.meta);
|
|
55
122
|
await writeText(briefAbs, briefMd);
|
|
@@ -67,12 +134,174 @@ export async function planCommand(options) {
|
|
|
67
134
|
for (const a of bundle.meta.assumptions)
|
|
68
135
|
log.info(` · ${a}`);
|
|
69
136
|
}
|
|
137
|
+
const wantsApply = options.applyPlanner === true || options.applyPlannerDryRun === true;
|
|
138
|
+
if (wantsApply) {
|
|
139
|
+
const applyDryRun = options.applyPlannerDryRun === true;
|
|
140
|
+
const exec = await resolvePlannerApplyExecution({
|
|
141
|
+
cwd,
|
|
142
|
+
nonInteractive,
|
|
143
|
+
fromLlmProvider: llmProviderForApply,
|
|
144
|
+
fromLlmModel: llmModelForApply,
|
|
145
|
+
cliProvider: options.provider,
|
|
146
|
+
cliModel: options.llmModel,
|
|
147
|
+
});
|
|
148
|
+
if (exec === null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const applyResult = await applyPlannerMarkdownFlow({
|
|
153
|
+
cwd,
|
|
154
|
+
planPromptMarkdown: promptMd,
|
|
155
|
+
provider: exec.provider,
|
|
156
|
+
model: exec.model,
|
|
157
|
+
dryRun: applyDryRun,
|
|
158
|
+
});
|
|
159
|
+
const applyInteractive = !nonInteractive && process.stdin.isTTY === true && !applyDryRun;
|
|
160
|
+
const hasWrittenFiles = !applyDryRun && applyResult.writtenRelativePaths.length > 0;
|
|
161
|
+
let doCommit = hasWrittenFiles && options.noGitCommit !== true;
|
|
162
|
+
if (doCommit && applyInteractive) {
|
|
163
|
+
log.blank();
|
|
164
|
+
log.step("Next: commit, push, and Notion (confirm each step)");
|
|
165
|
+
doCommit = await askYesNo({
|
|
166
|
+
message: "[1] Create a git commit for the planner output (docs/project + docs/tasks)?",
|
|
167
|
+
nonInteractive: false,
|
|
168
|
+
defaultValue: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
let committedOk = false;
|
|
172
|
+
if (doCommit) {
|
|
173
|
+
const gitReady = await ensureGitForPlannerCommit({
|
|
174
|
+
cwd,
|
|
175
|
+
interactive: applyInteractive,
|
|
176
|
+
});
|
|
177
|
+
if (!gitReady) {
|
|
178
|
+
doCommit = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (doCommit) {
|
|
182
|
+
try {
|
|
183
|
+
await commitPlannerGit(cwd, applyResult.writtenRelativePaths, "docs: planner output from vibeops plan (--apply-planner)");
|
|
184
|
+
log.ok("Git commit created for planner docs/tasks.");
|
|
185
|
+
committedOk = true;
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
189
|
+
log.warn(`Git commit did not run (${msg}). Review changes and commit manually.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let doPush = false;
|
|
193
|
+
if (committedOk) {
|
|
194
|
+
if (options.gitPush === true) {
|
|
195
|
+
doPush = true;
|
|
196
|
+
}
|
|
197
|
+
else if (applyInteractive) {
|
|
198
|
+
doPush = await askYesNo({
|
|
199
|
+
message: "[2] Push this commit to the remote (git push)? (If Yes, you may be offered `vibeops github init` to connect or create a GitHub repo / set `origin`.)",
|
|
200
|
+
nonInteractive: false,
|
|
201
|
+
defaultValue: false,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (doPush) {
|
|
205
|
+
await offerGithubInitBeforePush({ cwd, interactive: applyInteractive });
|
|
206
|
+
try {
|
|
207
|
+
await pushPlannerGit(cwd);
|
|
208
|
+
log.ok("git push complete.");
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
212
|
+
log.error(`git push failed: ${msg}`);
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (options.gitPush === true && !committedOk) {
|
|
218
|
+
log.warn("`--push` skipped (no successful commit: dry-run, --no-git-commit, you declined the commit prompt, commit error, or no files written).");
|
|
219
|
+
}
|
|
220
|
+
let doNotion = bundle.brief.useNotion === true && options.noNotionSync !== true && !applyDryRun;
|
|
221
|
+
if (doNotion && applyInteractive) {
|
|
222
|
+
doNotion = await askYesNo({
|
|
223
|
+
message: "[3] Run Notion sync now (project + TASK metadata)?",
|
|
224
|
+
nonInteractive: false,
|
|
225
|
+
defaultValue: true,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (doNotion) {
|
|
229
|
+
const hadErrBeforeNotion = process.exitCode === 1;
|
|
230
|
+
await runNotionSyncWithOptionalInit({ cwd, interactive: applyInteractive });
|
|
231
|
+
if (hadErrBeforeNotion) {
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
}
|
|
234
|
+
if (process.exitCode === 1 && !hadErrBeforeNotion) {
|
|
235
|
+
log.warn("Notion sync failed — fix integration and run `vibeops notion sync`.");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (bundle.brief.useNotion === true && applyDryRun) {
|
|
239
|
+
log.info(dim("Notion sync skipped for --apply-dry-run."));
|
|
240
|
+
}
|
|
241
|
+
else if (bundle.brief.useNotion === true && options.noNotionSync === true) {
|
|
242
|
+
log.info(dim("Notion sync skipped (--no-notion-sync)."));
|
|
243
|
+
}
|
|
244
|
+
else if (bundle.brief.useNotion === true && applyInteractive && !doNotion) {
|
|
245
|
+
log.info(dim("Notion sync skipped (you chose No)."));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
250
|
+
log.error(`Planner apply failed: ${msg}`);
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
70
255
|
log.blank();
|
|
71
256
|
log.info(bold("Next steps:"));
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
257
|
+
if (wantsApply && options.applyPlannerDryRun !== true) {
|
|
258
|
+
log.info(` 1) Open this repo in Cursor and run ${cyan("vibeops task start TASK-NNN")} for each backlog item.`);
|
|
259
|
+
log.info(` 2) The planning prompt is still at ${cyan(relDisplay(cwd, promptAbs))} if you need to re-run the planner.`);
|
|
260
|
+
log.info(` 3) To revise the brief only, edit ${cyan(relDisplay(cwd, briefAbs))} then:`);
|
|
261
|
+
log.info(` ${dim("vibeops plan --from " + DEFAULT_BRIEF_REL + " [--apply-planner …]")}`);
|
|
262
|
+
}
|
|
263
|
+
else if (wantsApply && options.applyPlannerDryRun === true) {
|
|
264
|
+
log.info(` 1) Re-run without ${cyan("--apply-dry-run")} to write files, commit, and (if enabled) Notion sync.`);
|
|
265
|
+
log.info(` 2) Or paste ${cyan(relDisplay(cwd, promptAbs))} into Cursor for a manual planner pass.`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
log.info(` 1) Open a new Cursor chat and paste the full contents of ${cyan(relDisplay(cwd, promptAbs))}.`);
|
|
269
|
+
log.info(` 2) Or run again with ${cyan("--apply-planner")}: the CLI calls the same LLM and writes ${cyan("docs/project/*")} + ${cyan("docs/tasks/*")} from the prompt (brief alone does not create TASK files).`);
|
|
270
|
+
log.info(` 3) Regenerate only the prompt from an edited brief:`);
|
|
271
|
+
log.info(` ${dim("vibeops plan --from " + DEFAULT_BRIEF_REL + " [--apply-planner …]")}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function resolvePlannerApplyExecution(inputs) {
|
|
275
|
+
if (inputs.fromLlmProvider !== undefined) {
|
|
276
|
+
return { provider: inputs.fromLlmProvider, model: inputs.fromLlmModel };
|
|
277
|
+
}
|
|
278
|
+
const tty = process.stdin.isTTY === true;
|
|
279
|
+
if (inputs.cliProvider !== undefined) {
|
|
280
|
+
const interactiveLogin = tty && !inputs.nonInteractive;
|
|
281
|
+
const provider = await pickPlanLlmProvider(inputs.cwd, inputs.cliProvider, { interactiveLogin });
|
|
282
|
+
if (provider === null)
|
|
283
|
+
return null;
|
|
284
|
+
let model = inputs.cliModel?.trim();
|
|
285
|
+
if (!model && tty && !inputs.nonInteractive) {
|
|
286
|
+
const picked = await pickPlanExecutionModel({
|
|
287
|
+
provider,
|
|
288
|
+
cwd: inputs.cwd,
|
|
289
|
+
interactive: true,
|
|
290
|
+
});
|
|
291
|
+
model = picked?.trim();
|
|
292
|
+
}
|
|
293
|
+
return { provider, model: model && model.length > 0 ? model : undefined };
|
|
294
|
+
}
|
|
295
|
+
if (!tty || inputs.nonInteractive) {
|
|
296
|
+
log.error("When using --apply-planner without an interactive LLM briefing in the same run, pass --provider openai | codex-oauth | cursor-agent (and optional --model / env defaults).");
|
|
297
|
+
process.exitCode = 1;
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const provider = await pickPlanLlmProvider(inputs.cwd, undefined, { interactiveLogin: true });
|
|
301
|
+
if (provider === null)
|
|
302
|
+
return null;
|
|
303
|
+
const model = await pickPlanExecutionModel({ provider, cwd: inputs.cwd, interactive: true });
|
|
304
|
+
return { provider, model };
|
|
76
305
|
}
|
|
77
306
|
async function loadFromFile(inputs) {
|
|
78
307
|
const { cwd, fromPath, nonInteractive, idea } = inputs;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
2
|
import { branchNameForTaskFile, findTaskFile, upsertGitContext, updateInlineStatus, } from "../lib/task.js";
|
|
3
|
-
import { detectDefaultBranch, gitBranchExists, gitCheckoutNewBranch, gitHeadCommit, readGitInfo, } from "../lib/git.js";
|
|
3
|
+
import { detectDefaultBranch, gitBranchExists, gitCheckoutNewBranch, gitGovernanceOnlyDirty, gitHeadCommit, readGitInfo, } from "../lib/git.js";
|
|
4
4
|
import { bold, cyan, dim, log } from "../lib/logger.js";
|
|
5
5
|
import { buildTaskPromptString } from "../lib/task-prompt.js";
|
|
6
6
|
import { projectPaths } from "../lib/paths.js";
|
|
@@ -25,9 +25,21 @@ export async function taskStartCommand(taskId, options = {}) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
if (git.dirty === true && options.allowDirty !== true) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const gov = await gitGovernanceOnlyDirty(cwd);
|
|
29
|
+
if (!gov.onlyGovernance) {
|
|
30
|
+
log.error("Git working tree is dirty. Commit or stash first, or rerun with --allow-dirty.");
|
|
31
|
+
if (gov.nonGovernancePaths.length > 0) {
|
|
32
|
+
log.info(dim(" Non-documentation changes:"));
|
|
33
|
+
for (const p of gov.nonGovernancePaths)
|
|
34
|
+
log.info(` · ${p}`);
|
|
35
|
+
}
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
log.warn("Uncommitted governance docs only (tasks / project / logs / .vibeops/state). Proceeding — commit when convenient.");
|
|
40
|
+
for (const p of gov.allPaths)
|
|
41
|
+
log.info(` ${dim("·")} ${p}`);
|
|
42
|
+
log.blank();
|
|
31
43
|
}
|
|
32
44
|
const baseBranch = git.branch ?? (await detectDefaultBranch(cwd)) ?? "main";
|
|
33
45
|
const baseCommit = (await gitHeadCommit(cwd)) ?? "";
|
package/dist/lib/brief.js
CHANGED
|
@@ -195,7 +195,7 @@ export async function gatherBrief(inputs) {
|
|
|
195
195
|
const meta = {
|
|
196
196
|
vibeopsVersion: VERSION,
|
|
197
197
|
generatedAt: new Date().toISOString(),
|
|
198
|
-
source: inputs.nonInteractive ? "non-interactive" : "interactive",
|
|
198
|
+
source: inputs.legacyWizard === true ? "legacy-wizard" : inputs.nonInteractive ? "non-interactive" : "interactive",
|
|
199
199
|
schemaVersion: PROJECT_BRIEF_SCHEMA_VERSION,
|
|
200
200
|
assumptions,
|
|
201
201
|
};
|
|
@@ -240,8 +240,15 @@ export function briefToMarkdown(brief, meta) {
|
|
|
240
240
|
lines.push("");
|
|
241
241
|
lines.push(`> Generated: ${meta.generatedAt} · VibeOps ${meta.vibeopsVersion} · Source: ${meta.source} · schemaVersion: ${meta.schemaVersion}`);
|
|
242
242
|
lines.push("");
|
|
243
|
-
lines.push(
|
|
244
|
-
|
|
243
|
+
lines.push(meta.source === "llm-openai" ||
|
|
244
|
+
meta.source === "llm-codex-oauth" ||
|
|
245
|
+
meta.source === "llm-cursor-agent"
|
|
246
|
+
? meta.source === "llm-codex-oauth"
|
|
247
|
+
? "This brief was produced by an **LLM planning session** using **Codex (ChatGPT OAuth)** (`codex login`, read from ~/.codex/auth.json). Review and edit it, then let the Planner Agent fill `docs/project/*`."
|
|
248
|
+
: meta.source === "llm-openai"
|
|
249
|
+
? "This brief was produced by an **LLM planning session** using an **OpenAI platform API key**. Review and edit it, then let the Planner Agent fill `docs/project/*`."
|
|
250
|
+
: "This brief was produced by an **LLM planning session** using the **Cursor Agent CLI**. Review and edit it, then let the Planner Agent fill `docs/project/*`."
|
|
251
|
+
: "This brief is the input for the Cursor **Planner Agent**. VibeOps collected these answers locally (or from `--from`) — the Planner Agent reads this brief, fills in `docs/project/*`, and creates the initial backlog.");
|
|
245
252
|
lines.push("");
|
|
246
253
|
for (const sec of SECTIONS) {
|
|
247
254
|
lines.push(`## ${sec.num}. ${sec.title}`);
|
|
@@ -330,7 +337,7 @@ export function parseBriefFromMarkdown(md) {
|
|
|
330
337
|
const meta = {
|
|
331
338
|
vibeopsVersion: recordedVersion,
|
|
332
339
|
generatedAt,
|
|
333
|
-
source: recordedSource
|
|
340
|
+
source: recordedSource,
|
|
334
341
|
schemaVersion: PROJECT_BRIEF_SCHEMA_VERSION,
|
|
335
342
|
assumptions,
|
|
336
343
|
};
|
package/dist/lib/git.js
CHANGED
|
@@ -167,6 +167,48 @@ function parsePorcelain(lines) {
|
|
|
167
167
|
}
|
|
168
168
|
return out;
|
|
169
169
|
}
|
|
170
|
+
/** Paths under these prefixes may stay uncommitted across `task done` / doc updates. */
|
|
171
|
+
const GOVERNANCE_DOC_REL_PREFIXES = [
|
|
172
|
+
"docs/tasks/",
|
|
173
|
+
"docs/project/",
|
|
174
|
+
"docs/logs/",
|
|
175
|
+
".vibeops/state/",
|
|
176
|
+
];
|
|
177
|
+
function normalizeRepoRelPath(p) {
|
|
178
|
+
return p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
179
|
+
}
|
|
180
|
+
export function isGovernanceDocumentationPath(repoRelativePath) {
|
|
181
|
+
const n = normalizeRepoRelPath(repoRelativePath);
|
|
182
|
+
for (const prefix of GOVERNANCE_DOC_REL_PREFIXES) {
|
|
183
|
+
if (n.startsWith(prefix))
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* When the index/worktree is dirty, classify whether every changed path is
|
|
190
|
+
* limited to governance docs (TASK files, project docs, logs, task state JSON).
|
|
191
|
+
* Used by `vibeops task start` so `task done --finalize` on main does not
|
|
192
|
+
* block the next TASK unless application code is also dirty.
|
|
193
|
+
*/
|
|
194
|
+
export async function gitGovernanceOnlyDirty(cwd) {
|
|
195
|
+
const lines = await gitStatusPorcelain(cwd);
|
|
196
|
+
const entries = parsePorcelain(lines);
|
|
197
|
+
const all = new Set();
|
|
198
|
+
for (const e of entries) {
|
|
199
|
+
all.add(e.path);
|
|
200
|
+
if (typeof e.origPath === "string" && e.origPath.length > 0) {
|
|
201
|
+
all.add(e.origPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const allPaths = [...all];
|
|
205
|
+
const nonGovernancePaths = allPaths.filter((p) => !isGovernanceDocumentationPath(p));
|
|
206
|
+
return {
|
|
207
|
+
allPaths,
|
|
208
|
+
nonGovernancePaths,
|
|
209
|
+
onlyGovernance: allPaths.length > 0 && nonGovernancePaths.length === 0,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
170
212
|
export async function gitWorkingTreeChangedFiles(cwd) {
|
|
171
213
|
const entries = parsePorcelain(await gitStatusPorcelain(cwd));
|
|
172
214
|
return entries.filter((e) => e.unstaged).map((e) => e.path);
|