@goodtek/vibeops 0.4.0 → 1.0.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 +198 -1
- package/README.md +62 -402
- package/dist/bootstrap/installer.js +4 -16
- package/dist/cli.js +52 -244
- package/dist/commands/init.js +3 -3
- package/dist/commands/status.js +145 -8
- package/dist/commands/task-add.js +100 -0
- package/dist/commands/task-done.js +117 -74
- package/dist/lib/config.js +2 -118
- package/dist/lib/git.js +190 -8
- package/dist/lib/llm-complete.js +62 -0
- package/dist/lib/paths.js +7 -3
- package/dist/lib/plan-llm-openai.js +0 -63
- package/dist/lib/resolve-task.js +34 -0
- package/dist/lib/slug.js +11 -0
- package/dist/lib/task-add-llm.js +81 -0
- package/dist/lib/task-context.js +68 -0
- package/dist/lib/task-done-llm.js +148 -0
- package/dist/lib/task-merge.js +115 -0
- package/dist/lib/task-scaffold.js +70 -92
- package/dist/lib/task-start.js +72 -0
- package/dist/lib/task-summary.js +12 -239
- package/dist/lib/task.js +80 -37
- package/dist/types/config.js +0 -16
- package/package.json +3 -4
- package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
- package/templates/.cursor/rules/02-git-safety.mdc +11 -0
- package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
- package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
- package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
- package/templates/AGENTS.md +26 -87
- package/templates/docs/logs/README.md +4 -35
- package/templates/docs/project/03-architecture.md +10 -29
- package/templates/docs/project/05-current-state.md +7 -27
- package/templates/docs/project/06-decisions.md +6 -17
- package/templates/docs/tasks/TASK-000-template.md +9 -44
- package/dist/agent/loader.js +0 -71
- package/dist/agent/prompt.js +0 -66
- package/dist/commands/agent-list.js +0 -32
- package/dist/commands/agent-prompt.js +0 -59
- package/dist/commands/agent-show.js +0 -26
- package/dist/commands/github-init.js +0 -554
- package/dist/commands/github-status.js +0 -164
- package/dist/commands/notion-init.js +0 -764
- package/dist/commands/notion-sync.js +0 -405
- package/dist/commands/notion-test.js +0 -595
- package/dist/commands/plan.js +0 -343
- package/dist/commands/task-check.js +0 -155
- package/dist/commands/task-generate.js +0 -206
- package/dist/commands/task-pull.js +0 -277
- package/dist/commands/task-rollback.js +0 -174
- package/dist/commands/task-start.js +0 -102
- package/dist/lib/brief.js +0 -356
- package/dist/lib/github-cli.js +0 -247
- package/dist/lib/notion-client.js +0 -459
- package/dist/lib/notion-discovery.js +0 -671
- package/dist/lib/notion-env.js +0 -140
- package/dist/lib/notion-mappers.js +0 -148
- package/dist/lib/notion-schema.js +0 -272
- package/dist/lib/notion-sync.js +0 -337
- package/dist/lib/notion-target.js +0 -247
- package/dist/lib/package-json.js +0 -133
- package/dist/lib/plan-apply-planner.js +0 -131
- package/dist/lib/plan-codex-pkce.js +0 -234
- package/dist/lib/plan-llm-models.js +0 -237
- package/dist/lib/plan-llm-normalize.js +0 -146
- package/dist/lib/plan-llm-session.js +0 -682
- package/dist/lib/plan-post-apply-setup.js +0 -116
- package/dist/lib/project-docs.js +0 -95
- package/dist/lib/prompt-builder.js +0 -135
- package/dist/lib/task-generator.js +0 -183
- package/dist/lib/task-prompt.js +0 -23
- package/dist/lib/task-pull.js +0 -354
- package/dist/status/collector.js +0 -103
- package/dist/status/format.js +0 -177
- package/dist/types/brief.js +0 -126
- package/templates/.cursor/rules/00-project-governance.mdc +0 -28
- package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
- package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
- package/templates/.cursor/rules/03-git-safety.mdc +0 -30
- package/templates/.cursor/rules/04-docs-update.mdc +0 -22
- package/templates/.vibeops/agents/architect.md +0 -47
- package/templates/.vibeops/agents/builder.md +0 -38
- package/templates/.vibeops/agents/docs.md +0 -54
- package/templates/.vibeops/agents/orchestrator.md +0 -40
- package/templates/.vibeops/agents/planner.md +0 -60
- package/templates/.vibeops/agents/recovery.md +0 -49
- package/templates/.vibeops/agents/reviewer.md +0 -47
- package/templates/.vibeops/agents/tester.md +0 -43
- package/templates/.vibeops/prompts/create-plan.md +0 -33
- package/templates/.vibeops/prompts/generate-tasks.md +0 -41
- package/templates/.vibeops/prompts/implement-task.md +0 -39
- package/templates/.vibeops/prompts/review-task.md +0 -34
- package/templates/.vibeops/prompts/rollback.md +0 -32
- package/templates/.vibeops/prompts/start-project.md +0 -39
- package/templates/.vibeops/workflows/notion-sync.md +0 -53
- package/templates/.vibeops/workflows/project-start.md +0 -73
- package/templates/.vibeops/workflows/rollback.md +0 -45
- package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
- package/templates/docs/project/00-overview.md +0 -27
- package/templates/docs/project/01-requirements.md +0 -30
- package/templates/docs/project/02-mvp-scope.md +0 -36
- package/templates/docs/project/04-tech-stack.md +0 -29
- package/templates/docs/project/07-backlog.md +0 -23
- package/templates/docs/project/08-env.md +0 -29
- package/templates/docs/project/09-deployment.md +0 -28
|
@@ -1,98 +1,141 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { askYesNo } from "../lib/inquirer-helpers.js";
|
|
2
3
|
import { readText } from "../lib/filesystem.js";
|
|
3
4
|
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
4
5
|
import { projectPaths } from "../lib/paths.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { resolveTask, taskNotFoundMessage } from "../lib/resolve-task.js";
|
|
7
|
+
import { mergeTaskBranch } from "../lib/task-merge.js";
|
|
8
|
+
import { applyTaskDoneMemory, fallbackResultSections, llmCompleteTaskDone, writeTaskResultSections, } from "../lib/task-done-llm.js";
|
|
9
|
+
import { relPath } from "../lib/task-context.js";
|
|
10
|
+
import { gitAddPaths, listWorkingTreeRelPaths, partitionPathsForAutoCommit, readGitInfo, runGit, } from "../lib/git.js";
|
|
11
|
+
import { hasNonEmptySection, markGitContextDone, parseTaskFilename, readGitContext, readTaskFile, updateInlineStatus, } from "../lib/task.js";
|
|
12
|
+
function commitMessageFor(taskId, title) {
|
|
13
|
+
const slug = title.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim() || taskId;
|
|
14
|
+
return `feat(${taskId.toLowerCase()}): ${slug}`;
|
|
9
15
|
}
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
+
async function tryCommitDirty(cwd, message, dryRun) {
|
|
17
|
+
const git = await readGitInfo(cwd);
|
|
18
|
+
if (!git.isRepo || git.dirty !== true)
|
|
19
|
+
return false;
|
|
20
|
+
const { committable, excluded } = partitionPathsForAutoCommit(await listWorkingTreeRelPaths(cwd), { unmerged: [] });
|
|
21
|
+
if (committable.length === 0)
|
|
22
|
+
return false;
|
|
23
|
+
if (excluded.length > 0) {
|
|
24
|
+
log.warn(`Skipping ${excluded.length} build artifact path(s) from auto-commit.`);
|
|
25
|
+
}
|
|
26
|
+
if (dryRun) {
|
|
27
|
+
log.info(dim(` would commit ${committable.length} path(s)`));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
await gitAddPaths(cwd, committable);
|
|
31
|
+
await runGit(cwd, ["commit", "-q", "-m", message]);
|
|
32
|
+
log.ok(`Committed: ${message}`);
|
|
33
|
+
return true;
|
|
16
34
|
}
|
|
17
|
-
export async function taskDoneCommand(
|
|
35
|
+
export async function taskDoneCommand(taskRef, options = {}) {
|
|
18
36
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
19
37
|
const paths = projectPaths(cwd);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
const dryRun = options.dryRun === true;
|
|
39
|
+
const interactive = !dryRun && process.stdin.isTTY === true && options.yes !== true;
|
|
40
|
+
const resolved = await resolveTask(paths, cwd, taskRef);
|
|
41
|
+
if (resolved === null) {
|
|
42
|
+
log.error(taskNotFoundMessage(taskRef));
|
|
23
43
|
process.exitCode = 1;
|
|
24
44
|
return;
|
|
25
45
|
}
|
|
46
|
+
const { taskId, taskFile } = resolved;
|
|
26
47
|
const meta = await readTaskFile(taskFile);
|
|
27
48
|
const body = await readText(taskFile);
|
|
28
|
-
|
|
29
|
-
log.info(`
|
|
30
|
-
log.info(` ${dim("
|
|
31
|
-
log.info(` ${dim("status")} ${statusDisplay(meta.status)}`);
|
|
49
|
+
const relFile = relPath(cwd, taskFile);
|
|
50
|
+
log.info(bold(`vibeops task done ${taskId}`));
|
|
51
|
+
log.info(` ${dim("file")} ${relFile}`);
|
|
32
52
|
log.blank();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
let resultOk = hasNonEmptySection(body, "Result");
|
|
54
|
+
let testOk = hasNonEmptySection(body, "Test Result");
|
|
55
|
+
if (!resultOk || !testOk) {
|
|
56
|
+
log.step("LLM — Result / Test Result + project memory");
|
|
57
|
+
if (dryRun) {
|
|
58
|
+
log.info(dim(" would call LLM and patch docs/project/*"));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const patch = await llmCompleteTaskDone({
|
|
62
|
+
cwd,
|
|
63
|
+
taskId,
|
|
64
|
+
taskTitle: meta.title,
|
|
65
|
+
taskBody: body,
|
|
66
|
+
taskFileRel: relFile,
|
|
67
|
+
});
|
|
68
|
+
if (patch !== null) {
|
|
69
|
+
await writeTaskResultSections(taskFile, patch.result, patch.testResult);
|
|
70
|
+
log.ok(`Result / Test Result updated (${patch.provider})`);
|
|
71
|
+
const memory = await applyTaskDoneMemory(cwd, patch);
|
|
72
|
+
for (const p of memory.updated)
|
|
73
|
+
log.ok(`Updated ${p}`);
|
|
74
|
+
for (const p of memory.skipped)
|
|
75
|
+
log.skip(`Skipped ${p}`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
log.warn("LLM unavailable — using git diff fallback for Result sections.");
|
|
79
|
+
const fb = await fallbackResultSections(cwd, taskId, taskFile);
|
|
80
|
+
await writeTaskResultSections(taskFile, fb.result, fb.testResult);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const body2 = dryRun ? body : await readText(taskFile);
|
|
84
|
+
resultOk = hasNonEmptySection(body2, "Result");
|
|
85
|
+
testOk = hasNonEmptySection(body2, "Test Result");
|
|
45
86
|
}
|
|
87
|
+
log.info(bold("Sections"));
|
|
88
|
+
log.info(` ${resultOk ? green("✓") : yellow("·")} Result`);
|
|
89
|
+
log.info(` ${testOk ? green("✓") : yellow("·")} Test Result`);
|
|
46
90
|
log.blank();
|
|
47
|
-
if (
|
|
48
|
-
log.error(
|
|
91
|
+
if (!dryRun && (!resultOk || !testOk)) {
|
|
92
|
+
log.error("Fill Result and Test Result in the TASK file, then rerun.");
|
|
49
93
|
process.exitCode = 1;
|
|
50
94
|
return;
|
|
51
95
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
log.info(
|
|
58
|
-
log.
|
|
59
|
-
|
|
60
|
-
else {
|
|
61
|
-
await updateInlineStatus(taskFile, target);
|
|
62
|
-
log.ok(`Status → ${targetDisplay} (${relOrAbs(cwd, taskFile)})`);
|
|
63
|
-
log.blank();
|
|
96
|
+
if (dryRun) {
|
|
97
|
+
log.info(bold("dry-run — would:"));
|
|
98
|
+
if (!resultOk || !testOk) {
|
|
99
|
+
log.info(" · LLM fill Result / Test Result + patch docs/project/*");
|
|
100
|
+
}
|
|
101
|
+
log.info(" · Status → Done");
|
|
102
|
+
log.info(" · commit if dirty, merge task branch (unless --no-merge)");
|
|
103
|
+
return;
|
|
64
104
|
}
|
|
65
|
-
|
|
105
|
+
await updateInlineStatus(taskFile, "done");
|
|
106
|
+
await markGitContextDone(taskFile);
|
|
107
|
+
log.ok("Status → Done");
|
|
66
108
|
const parts = parseTaskFilename(taskFile);
|
|
67
|
-
const commitMsg = commitMessageFor(parts.id, meta.title
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
const commitMsg = commitMessageFor(parts.id, meta.title);
|
|
110
|
+
await tryCommitDirty(cwd, commitMsg, false);
|
|
111
|
+
const gitCtx = await readGitContext(taskFile);
|
|
112
|
+
if (options.noMerge !== true && gitCtx !== null) {
|
|
113
|
+
let doMerge = options.yes === true || !interactive;
|
|
114
|
+
if (interactive) {
|
|
115
|
+
doMerge = await askYesNo({
|
|
116
|
+
message: `Merge into ${gitCtx.baseBranch} and delete ${gitCtx.taskBranch}?`,
|
|
117
|
+
nonInteractive: false,
|
|
118
|
+
defaultValue: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (doMerge) {
|
|
122
|
+
const mergeResult = await mergeTaskBranch({
|
|
123
|
+
cwd,
|
|
124
|
+
taskId: meta.id,
|
|
125
|
+
taskTitle: meta.title,
|
|
126
|
+
gitCtx,
|
|
127
|
+
allowDirty: options.allowDirty,
|
|
128
|
+
});
|
|
129
|
+
if (!mergeResult.ok)
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
log.info(dim("Merge skipped."));
|
|
134
|
+
}
|
|
75
135
|
}
|
|
76
|
-
|
|
77
|
-
|
|
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)")}`);
|
|
136
|
+
else if (gitCtx === null) {
|
|
137
|
+
log.info(dim("No Git Context — merge skipped."));
|
|
90
138
|
}
|
|
91
139
|
log.blank();
|
|
92
|
-
|
|
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
|
-
}
|
|
140
|
+
log.info(`Next: ${cyan("vibeops task add")} or ${cyan("vibeops status")}`);
|
|
98
141
|
}
|
package/dist/lib/config.js
CHANGED
|
@@ -2,39 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { readTextOrNull, writeText } from "./filesystem.js";
|
|
3
3
|
import { VIBEOPS_CONFIG_FILE } from "./paths.js";
|
|
4
4
|
import { VERSION } from "../version.js";
|
|
5
|
-
import {
|
|
6
|
-
function parseNotionSection(raw) {
|
|
7
|
-
if (raw === null || typeof raw !== "object")
|
|
8
|
-
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;
|
|
18
|
-
}
|
|
19
|
-
function parseGithubVisibility(raw) {
|
|
20
|
-
if (raw === "public" || raw === "private")
|
|
21
|
-
return raw;
|
|
22
|
-
return "";
|
|
23
|
-
}
|
|
24
|
-
function parseGithubSection(raw) {
|
|
25
|
-
if (raw === null || typeof raw !== "object")
|
|
26
|
-
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
|
-
};
|
|
37
|
-
}
|
|
5
|
+
import { VIBEOPS_CONFIG_SCHEMA_VERSION } from "../types/config.js";
|
|
38
6
|
export async function readConfig(root) {
|
|
39
7
|
const text = await readTextOrNull(join(root, VIBEOPS_CONFIG_FILE));
|
|
40
8
|
if (text === null)
|
|
@@ -45,19 +13,12 @@ export async function readConfig(root) {
|
|
|
45
13
|
typeof parsed.vibeopsVersion === "string" &&
|
|
46
14
|
typeof parsed.createdAt === "string" &&
|
|
47
15
|
parsed.schemaVersion === VIBEOPS_CONFIG_SCHEMA_VERSION) {
|
|
48
|
-
|
|
49
|
-
const github = parseGithubSection(parsed.github);
|
|
50
|
-
const config = {
|
|
16
|
+
return {
|
|
51
17
|
name: parsed.name,
|
|
52
18
|
vibeopsVersion: parsed.vibeopsVersion,
|
|
53
19
|
schemaVersion: VIBEOPS_CONFIG_SCHEMA_VERSION,
|
|
54
20
|
createdAt: parsed.createdAt,
|
|
55
21
|
};
|
|
56
|
-
if (notion)
|
|
57
|
-
config.notion = notion;
|
|
58
|
-
if (github)
|
|
59
|
-
config.github = github;
|
|
60
|
-
return config;
|
|
61
22
|
}
|
|
62
23
|
return null;
|
|
63
24
|
}
|
|
@@ -79,80 +40,3 @@ export function configToJson(config) {
|
|
|
79
40
|
export async function writeConfig(root, config) {
|
|
80
41
|
await writeText(join(root, VIBEOPS_CONFIG_FILE), configToJson(config));
|
|
81
42
|
}
|
|
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
|
-
/**
|
|
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).
|
|
129
|
-
*/
|
|
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 };
|
|
158
|
-
}
|
package/dist/lib/git.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { dim, log } from "./logger.js";
|
|
3
4
|
const exec = promisify(execFile);
|
|
5
|
+
/** Avoid `maxBuffer` errors on large `git commit` / `git add` output. */
|
|
6
|
+
const DEFAULT_GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
4
7
|
async function tryGit(cwd, args) {
|
|
5
8
|
try {
|
|
6
9
|
const { stdout } = await exec("git", args, { cwd });
|
|
@@ -10,8 +13,11 @@ async function tryGit(cwd, args) {
|
|
|
10
13
|
return null;
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
|
-
export async function runGit(cwd, args) {
|
|
14
|
-
const { stdout, stderr } = await exec("git", args, {
|
|
16
|
+
export async function runGit(cwd, args, options = {}) {
|
|
17
|
+
const { stdout, stderr } = await exec("git", args, {
|
|
18
|
+
cwd,
|
|
19
|
+
maxBuffer: options.maxBuffer ?? DEFAULT_GIT_MAX_BUFFER,
|
|
20
|
+
});
|
|
15
21
|
return { stdout, stderr };
|
|
16
22
|
}
|
|
17
23
|
export async function isGitRepository(cwd) {
|
|
@@ -83,14 +89,90 @@ export async function gitCreateBranch(cwd, name, startPoint) {
|
|
|
83
89
|
args.push(startPoint);
|
|
84
90
|
await runGit(cwd, args);
|
|
85
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* When only governance / VibeOps paths are dirty, stash them so `git switch` can proceed.
|
|
94
|
+
* Callers that switch branches should restore with {@link restoreGovernanceStashAfterSwitch}.
|
|
95
|
+
*/
|
|
96
|
+
export async function stashGovernanceIfBlocking(cwd) {
|
|
97
|
+
const git = await readGitInfo(cwd);
|
|
98
|
+
if (git.dirty !== true)
|
|
99
|
+
return false;
|
|
100
|
+
const gov = await gitGovernanceOnlyDirty(cwd);
|
|
101
|
+
if (!gov.onlyGovernance || gov.allPaths.length === 0)
|
|
102
|
+
return false;
|
|
103
|
+
const entries = parsePorcelain(await gitStatusPorcelain(cwd));
|
|
104
|
+
const tracked = [];
|
|
105
|
+
const untracked = [];
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
if (!isGovernanceDocumentationPath(e.path))
|
|
108
|
+
continue;
|
|
109
|
+
if (e.untracked)
|
|
110
|
+
untracked.push(e.path);
|
|
111
|
+
else
|
|
112
|
+
tracked.push(e.path);
|
|
113
|
+
}
|
|
114
|
+
let didStash = false;
|
|
115
|
+
if (tracked.length > 0) {
|
|
116
|
+
await runGit(cwd, [
|
|
117
|
+
"stash",
|
|
118
|
+
"push",
|
|
119
|
+
"-m",
|
|
120
|
+
"vibeops: governance before branch switch",
|
|
121
|
+
"--",
|
|
122
|
+
...tracked,
|
|
123
|
+
]);
|
|
124
|
+
didStash = true;
|
|
125
|
+
}
|
|
126
|
+
if (untracked.length > 0) {
|
|
127
|
+
try {
|
|
128
|
+
await runGit(cwd, [
|
|
129
|
+
"stash",
|
|
130
|
+
"push",
|
|
131
|
+
"-u",
|
|
132
|
+
"-m",
|
|
133
|
+
"vibeops: governance untracked before branch switch",
|
|
134
|
+
"--",
|
|
135
|
+
...untracked,
|
|
136
|
+
]);
|
|
137
|
+
didStash = true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Untracked generated files (e.g. cursor-implement-*.md) may not stash; switch can still proceed.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return didStash;
|
|
144
|
+
}
|
|
145
|
+
/** Re-apply governance paths stashed for a branch switch (e.g. new `docs/tasks/*.md`). */
|
|
146
|
+
export async function restoreGovernanceStashAfterSwitch(cwd, didStash) {
|
|
147
|
+
if (!didStash)
|
|
148
|
+
return;
|
|
149
|
+
try {
|
|
150
|
+
await runGit(cwd, ["stash", "pop"]);
|
|
151
|
+
log.info(dim("Restored stashed governance paths on the task branch."));
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
155
|
+
log.warn(`Could not restore stashed governance files (${msg}). Run ${dim("git stash pop")} manually.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
86
158
|
export async function gitCheckout(cwd, ref) {
|
|
87
|
-
await
|
|
159
|
+
const stashed = await stashGovernanceIfBlocking(cwd);
|
|
160
|
+
if (stashed) {
|
|
161
|
+
log.info(dim("Stashed governance-only changes (.vibeops/, docs/) before branch switch."));
|
|
162
|
+
}
|
|
163
|
+
await runGit(cwd, ["switch", ref]);
|
|
164
|
+
await restoreGovernanceStashAfterSwitch(cwd, stashed);
|
|
88
165
|
}
|
|
89
166
|
export async function gitCheckoutNewBranch(cwd, name, startPoint) {
|
|
90
|
-
const
|
|
167
|
+
const stashed = await stashGovernanceIfBlocking(cwd);
|
|
168
|
+
if (stashed) {
|
|
169
|
+
log.info(dim("Stashed governance-only changes (.vibeops/, docs/) before branch switch."));
|
|
170
|
+
}
|
|
171
|
+
const args = ["switch", "-c", name];
|
|
91
172
|
if (typeof startPoint === "string" && startPoint.length > 0)
|
|
92
173
|
args.push(startPoint);
|
|
93
174
|
await runGit(cwd, args);
|
|
175
|
+
await restoreGovernanceStashAfterSwitch(cwd, stashed);
|
|
94
176
|
}
|
|
95
177
|
export async function gitDeleteBranch(cwd, name, opts = {}) {
|
|
96
178
|
const flag = opts.force === true ? "-D" : "-d";
|
|
@@ -167,12 +249,12 @@ function parsePorcelain(lines) {
|
|
|
167
249
|
}
|
|
168
250
|
return out;
|
|
169
251
|
}
|
|
170
|
-
/** Paths under these prefixes may stay uncommitted across `
|
|
252
|
+
/** Paths under these prefixes may stay uncommitted across `start` / `done` / doc updates. */
|
|
171
253
|
const GOVERNANCE_DOC_REL_PREFIXES = [
|
|
172
254
|
"docs/tasks/",
|
|
173
255
|
"docs/project/",
|
|
174
256
|
"docs/logs/",
|
|
175
|
-
".vibeops/
|
|
257
|
+
".vibeops/",
|
|
176
258
|
];
|
|
177
259
|
function normalizeRepoRelPath(p) {
|
|
178
260
|
return p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
@@ -185,10 +267,110 @@ export function isGovernanceDocumentationPath(repoRelativePath) {
|
|
|
185
267
|
}
|
|
186
268
|
return false;
|
|
187
269
|
}
|
|
270
|
+
/** Build artifacts / deps — never auto-committed by `vibeops done` cleanup. */
|
|
271
|
+
const AUTO_COMMIT_EXCLUDED_PREFIXES = [
|
|
272
|
+
"node_modules/",
|
|
273
|
+
".next/",
|
|
274
|
+
".pnpm-store/",
|
|
275
|
+
"dist/",
|
|
276
|
+
"build/",
|
|
277
|
+
".turbo/",
|
|
278
|
+
"coverage/",
|
|
279
|
+
".vercel/",
|
|
280
|
+
".output/",
|
|
281
|
+
".cache/",
|
|
282
|
+
];
|
|
283
|
+
const AUTO_COMMIT_EXCLUDED_EXACT = new Set([
|
|
284
|
+
"node_modules",
|
|
285
|
+
".next",
|
|
286
|
+
"dist",
|
|
287
|
+
"build",
|
|
288
|
+
]);
|
|
289
|
+
export function isAutoCommitExcludedPath(repoRelativePath) {
|
|
290
|
+
const n = normalizeRepoRelPath(repoRelativePath);
|
|
291
|
+
if (AUTO_COMMIT_EXCLUDED_EXACT.has(n))
|
|
292
|
+
return true;
|
|
293
|
+
for (const prefix of AUTO_COMMIT_EXCLUDED_PREFIXES) {
|
|
294
|
+
if (n.startsWith(prefix))
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
export async function gitMergeInProgress(cwd) {
|
|
300
|
+
return (await tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])) !== null;
|
|
301
|
+
}
|
|
302
|
+
/** Conflict paths from an in-progress merge (`git diff --diff-filter=U`). */
|
|
303
|
+
export async function listUnmergedRelPaths(cwd) {
|
|
304
|
+
const res = await tryGit(cwd, ["diff", "--name-only", "--diff-filter=U"]);
|
|
305
|
+
if (res === null)
|
|
306
|
+
return [];
|
|
307
|
+
return res.stdout
|
|
308
|
+
.split("\n")
|
|
309
|
+
.map((p) => p.trim())
|
|
310
|
+
.filter((p) => p.length > 0);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* During a merge, take the incoming branch version for governance / VibeOps paths
|
|
314
|
+
* (typical after `vibeops done` merge conflicts on `.vibeops/generated/*`).
|
|
315
|
+
*/
|
|
316
|
+
export async function tryResolveGovernanceUnmerged(cwd) {
|
|
317
|
+
const unmerged = await listUnmergedRelPaths(cwd);
|
|
318
|
+
const resolved = [];
|
|
319
|
+
for (const p of unmerged) {
|
|
320
|
+
if (!isGovernanceDocumentationPath(p))
|
|
321
|
+
continue;
|
|
322
|
+
try {
|
|
323
|
+
await runGit(cwd, ["checkout", "--theirs", "--", p]);
|
|
324
|
+
await runGit(cwd, ["add", "--", p]);
|
|
325
|
+
resolved.push(p);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// skip paths that cannot be auto-resolved
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return resolved;
|
|
332
|
+
}
|
|
333
|
+
export async function listWorkingTreeRelPaths(cwd) {
|
|
334
|
+
const entries = parsePorcelain(await gitStatusPorcelain(cwd));
|
|
335
|
+
const all = new Set();
|
|
336
|
+
for (const e of entries) {
|
|
337
|
+
all.add(e.path);
|
|
338
|
+
if (typeof e.origPath === "string" && e.origPath.length > 0) {
|
|
339
|
+
all.add(e.origPath);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return [...all];
|
|
343
|
+
}
|
|
344
|
+
export function partitionPathsForAutoCommit(paths, opts = {}) {
|
|
345
|
+
const unmergedSet = new Set(opts.unmerged ?? []);
|
|
346
|
+
const committable = [];
|
|
347
|
+
const excluded = [];
|
|
348
|
+
const unmerged = [];
|
|
349
|
+
for (const p of paths) {
|
|
350
|
+
if (unmergedSet.has(p)) {
|
|
351
|
+
unmerged.push(p);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (isAutoCommitExcludedPath(p))
|
|
355
|
+
excluded.push(p);
|
|
356
|
+
else
|
|
357
|
+
committable.push(p);
|
|
358
|
+
}
|
|
359
|
+
return { committable, excluded, unmerged };
|
|
360
|
+
}
|
|
361
|
+
export async function gitAddPaths(cwd, paths) {
|
|
362
|
+
if (paths.length === 0)
|
|
363
|
+
return;
|
|
364
|
+
const chunkSize = 80;
|
|
365
|
+
for (let i = 0; i < paths.length; i += chunkSize) {
|
|
366
|
+
const slice = paths.slice(i, i + chunkSize);
|
|
367
|
+
await runGit(cwd, ["add", "--", ...slice]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
188
370
|
/**
|
|
189
371
|
* When the index/worktree is dirty, classify whether every changed path is
|
|
190
|
-
* limited to governance docs
|
|
191
|
-
* Used
|
|
372
|
+
* limited to governance docs and VibeOps metadata (`.vibeops/**`, docs/tasks, etc.).
|
|
373
|
+
* Used when switching task branches so governance paths are not left off-disk after switch.
|
|
192
374
|
* block the next TASK unless application code is also dirty.
|
|
193
375
|
*/
|
|
194
376
|
export async function gitGovernanceOnlyDirty(cwd) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { probeCodexOAuthFile } from "./plan-codex-auth.js";
|
|
2
|
+
import { codexOAuthTextCompletion } from "./plan-codex-responses.js";
|
|
3
|
+
import { cursorAgentPrint, extractAgentAssistantText } from "./plan-llm-cursor-agent.js";
|
|
4
|
+
import { probeCursorAgentCli, probeOpenAiApiKey } from "./plan-llm-detect.js";
|
|
5
|
+
import { extractJsonObject, openAiChatCompletionJson, openAiChatCompletionText, } from "./plan-llm-openai.js";
|
|
6
|
+
/** Non-interactive: Codex OAuth → Cursor Agent → OpenAI API key (same sources as `vibeops plan`). */
|
|
7
|
+
export async function resolveAvailableLlmProvider(cwd) {
|
|
8
|
+
const codex = await probeCodexOAuthFile();
|
|
9
|
+
if (codex.ok)
|
|
10
|
+
return "codex-oauth";
|
|
11
|
+
const cursor = await probeCursorAgentCli(cwd);
|
|
12
|
+
if (cursor.ok)
|
|
13
|
+
return "cursor-agent";
|
|
14
|
+
const openai = await probeOpenAiApiKey();
|
|
15
|
+
if (openai.ok)
|
|
16
|
+
return "openai";
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function formatMessagesForCursor(messages) {
|
|
20
|
+
return messages
|
|
21
|
+
.map((m) => `### ${m.role}\n\n${m.content}`)
|
|
22
|
+
.join("\n\n---\n\n");
|
|
23
|
+
}
|
|
24
|
+
export async function llmCompleteText(messages, opts) {
|
|
25
|
+
const provider = opts.provider ?? (await resolveAvailableLlmProvider(opts.cwd));
|
|
26
|
+
if (provider === null) {
|
|
27
|
+
throw new Error("No LLM provider available. Use Codex OAuth (~/.codex/auth.json), Cursor Agent CLI (`agent login`), or OPENAI_API_KEY.");
|
|
28
|
+
}
|
|
29
|
+
if (provider === "openai") {
|
|
30
|
+
const text = await openAiChatCompletionText({ messages, model: opts.model });
|
|
31
|
+
return { text, provider };
|
|
32
|
+
}
|
|
33
|
+
if (provider === "codex-oauth") {
|
|
34
|
+
const text = await codexOAuthTextCompletion(messages, { model: opts.model });
|
|
35
|
+
return { text, provider };
|
|
36
|
+
}
|
|
37
|
+
const raw = await cursorAgentPrint({
|
|
38
|
+
cwd: opts.cwd,
|
|
39
|
+
prompt: formatMessagesForCursor(messages),
|
|
40
|
+
model: opts.model,
|
|
41
|
+
});
|
|
42
|
+
return { text: extractAgentAssistantText(raw), provider: "cursor-agent" };
|
|
43
|
+
}
|
|
44
|
+
export async function llmCompleteJson(messages, opts) {
|
|
45
|
+
const provider = opts.provider ?? (await resolveAvailableLlmProvider(opts.cwd));
|
|
46
|
+
if (provider === null) {
|
|
47
|
+
throw new Error("No LLM provider available. Use Codex OAuth (~/.codex/auth.json), Cursor Agent CLI (`agent login`), or OPENAI_API_KEY.");
|
|
48
|
+
}
|
|
49
|
+
if (provider === "openai") {
|
|
50
|
+
const text = await openAiChatCompletionJson({ messages, model: opts.model });
|
|
51
|
+
return { text, provider };
|
|
52
|
+
}
|
|
53
|
+
const jsonMessages = [
|
|
54
|
+
...messages,
|
|
55
|
+
{
|
|
56
|
+
role: "user",
|
|
57
|
+
content: "Reply with a single JSON object only. No markdown code fences or extra prose.",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
const { text: raw } = await llmCompleteText(jsonMessages, { ...opts, provider });
|
|
61
|
+
return { text: extractJsonObject(raw), provider };
|
|
62
|
+
}
|