@goodtek/vibeops 0.2.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 +28 -0
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/dist/agent/loader.js +71 -0
- package/dist/agent/prompt.js +66 -0
- package/dist/bootstrap/installer.js +149 -0
- package/dist/bootstrap/manifest.js +15 -0
- package/dist/bootstrap/substitute.js +35 -0
- package/dist/cli.js +241 -0
- package/dist/commands/agent-list.js +32 -0
- package/dist/commands/agent-prompt.js +59 -0
- package/dist/commands/agent-show.js +26 -0
- package/dist/commands/github-init.js +554 -0
- package/dist/commands/github-status.js +164 -0
- package/dist/commands/init.js +179 -0
- package/dist/commands/notion-init.js +764 -0
- package/dist/commands/notion-sync.js +405 -0
- package/dist/commands/notion-test.js +595 -0
- package/dist/commands/plan.js +114 -0
- package/dist/commands/status.js +17 -0
- package/dist/commands/task-check.js +155 -0
- package/dist/commands/task-done.js +98 -0
- package/dist/commands/task-generate.js +206 -0
- package/dist/commands/task-pull.js +277 -0
- package/dist/commands/task-rollback.js +174 -0
- package/dist/commands/task-start.js +90 -0
- package/dist/lib/brief.js +349 -0
- package/dist/lib/config.js +158 -0
- package/dist/lib/filesystem.js +67 -0
- package/dist/lib/git.js +237 -0
- package/dist/lib/github-cli.js +247 -0
- package/dist/lib/inquirer-helpers.js +111 -0
- package/dist/lib/logger.js +42 -0
- package/dist/lib/notion-client.js +459 -0
- package/dist/lib/notion-discovery.js +671 -0
- package/dist/lib/notion-env.js +140 -0
- package/dist/lib/notion-mappers.js +148 -0
- package/dist/lib/notion-schema.js +272 -0
- package/dist/lib/notion-sync.js +337 -0
- package/dist/lib/notion-target.js +247 -0
- package/dist/lib/package-json.js +133 -0
- package/dist/lib/paths.js +26 -0
- package/dist/lib/project-docs.js +95 -0
- package/dist/lib/prompt-builder.js +125 -0
- package/dist/lib/task-generator.js +183 -0
- package/dist/lib/task-prompt.js +23 -0
- package/dist/lib/task-pull.js +354 -0
- package/dist/lib/task-scaffold.js +128 -0
- package/dist/lib/task-summary.js +276 -0
- package/dist/lib/task.js +364 -0
- package/dist/status/collector.js +103 -0
- package/dist/status/format.js +177 -0
- package/dist/types/brief.js +126 -0
- package/dist/types/config.js +17 -0
- package/dist/types/task.js +1 -0
- package/dist/version.js +8 -0
- package/package.json +61 -0
- package/templates/.cursor/rules/00-project-governance.mdc +28 -0
- package/templates/.cursor/rules/01-agent-orchestration.mdc +48 -0
- package/templates/.cursor/rules/02-task-workflow.mdc +38 -0
- package/templates/.cursor/rules/03-git-safety.mdc +30 -0
- package/templates/.cursor/rules/04-docs-update.mdc +22 -0
- package/templates/.vibeops/agents/architect.md +47 -0
- package/templates/.vibeops/agents/builder.md +38 -0
- package/templates/.vibeops/agents/docs.md +54 -0
- package/templates/.vibeops/agents/orchestrator.md +40 -0
- package/templates/.vibeops/agents/planner.md +60 -0
- package/templates/.vibeops/agents/recovery.md +49 -0
- package/templates/.vibeops/agents/reviewer.md +47 -0
- package/templates/.vibeops/agents/tester.md +43 -0
- package/templates/.vibeops/prompts/create-plan.md +33 -0
- package/templates/.vibeops/prompts/generate-tasks.md +41 -0
- package/templates/.vibeops/prompts/implement-task.md +39 -0
- package/templates/.vibeops/prompts/review-task.md +34 -0
- package/templates/.vibeops/prompts/rollback.md +32 -0
- package/templates/.vibeops/prompts/start-project.md +39 -0
- package/templates/.vibeops/workflows/notion-sync.md +53 -0
- package/templates/.vibeops/workflows/project-start.md +73 -0
- package/templates/.vibeops/workflows/rollback.md +45 -0
- package/templates/.vibeops/workflows/task-lifecycle.md +71 -0
- package/templates/AGENTS.md +98 -0
- package/templates/docs/logs/README.md +38 -0
- package/templates/docs/project/00-overview.md +27 -0
- package/templates/docs/project/01-requirements.md +30 -0
- package/templates/docs/project/02-mvp-scope.md +36 -0
- package/templates/docs/project/03-architecture.md +34 -0
- package/templates/docs/project/04-tech-stack.md +29 -0
- package/templates/docs/project/05-current-state.md +35 -0
- package/templates/docs/project/06-decisions.md +20 -0
- package/templates/docs/project/07-backlog.md +23 -0
- package/templates/docs/project/08-env.md +29 -0
- package/templates/docs/project/09-deployment.md +28 -0
- package/templates/docs/tasks/TASK-000-template.md +72 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readTextOrNull, writeText } from "./filesystem.js";
|
|
3
|
+
const PACKAGE_JSON_FILE = "package.json";
|
|
4
|
+
export async function readPackageJson(cwd) {
|
|
5
|
+
const abs = join(cwd, PACKAGE_JSON_FILE);
|
|
6
|
+
const raw = await readTextOrNull(abs);
|
|
7
|
+
if (raw === null)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const data = JSON.parse(raw);
|
|
11
|
+
return { path: abs, raw, data };
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Read the canonical repository URL string, regardless of shape. */
|
|
18
|
+
export function readRepositoryUrl(pkg) {
|
|
19
|
+
const r = pkg.repository;
|
|
20
|
+
if (typeof r === "string")
|
|
21
|
+
return r;
|
|
22
|
+
if (r !== null && typeof r === "object" && typeof r.url === "string")
|
|
23
|
+
return r.url;
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
export function readBugsUrl(pkg) {
|
|
27
|
+
const b = pkg.bugs;
|
|
28
|
+
if (typeof b === "string")
|
|
29
|
+
return b;
|
|
30
|
+
if (b !== null && typeof b === "object" && typeof b.url === "string")
|
|
31
|
+
return b.url;
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
export function readHomepage(pkg) {
|
|
35
|
+
return typeof pkg.homepage === "string" ? pkg.homepage : "";
|
|
36
|
+
}
|
|
37
|
+
function detectIndent(raw) {
|
|
38
|
+
// Look at the first indented line.
|
|
39
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
40
|
+
const m = /^(\s+)\S/.exec(line);
|
|
41
|
+
if (m === null)
|
|
42
|
+
continue;
|
|
43
|
+
const indent = m[1];
|
|
44
|
+
if (indent.startsWith("\t"))
|
|
45
|
+
return 0;
|
|
46
|
+
if (indent.length > 0)
|
|
47
|
+
return indent.length;
|
|
48
|
+
}
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
function stringifyKeepingTrailingNewline(data, raw, indent) {
|
|
52
|
+
const trailing = raw.endsWith("\n") ? "\n" : "";
|
|
53
|
+
return `${JSON.stringify(data, null, indent || 2)}${trailing}`;
|
|
54
|
+
}
|
|
55
|
+
export function buildRepositoryFieldsPatch({ owner, repo, }) {
|
|
56
|
+
return {
|
|
57
|
+
repositoryUrl: `git+https://github.com/${owner}/${repo}.git`,
|
|
58
|
+
homepage: `https://github.com/${owner}/${repo}#readme`,
|
|
59
|
+
bugsUrl: `https://github.com/${owner}/${repo}/issues`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function updatePackageRepositoryFields(input) {
|
|
63
|
+
const read = await readPackageJson(input.cwd);
|
|
64
|
+
const path = join(input.cwd, PACKAGE_JSON_FILE);
|
|
65
|
+
if (read === null) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
reason: "missing",
|
|
69
|
+
diffs: [],
|
|
70
|
+
written: false,
|
|
71
|
+
path,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const patch = buildRepositoryFieldsPatch(input.patch);
|
|
75
|
+
const before = {
|
|
76
|
+
repositoryUrl: readRepositoryUrl(read.data),
|
|
77
|
+
homepage: readHomepage(read.data),
|
|
78
|
+
bugsUrl: readBugsUrl(read.data),
|
|
79
|
+
};
|
|
80
|
+
const next = { ...read.data };
|
|
81
|
+
// repository: always normalize to object shape with type=git.
|
|
82
|
+
const prevRepo = read.data.repository;
|
|
83
|
+
const repoObject = prevRepo !== null && typeof prevRepo === "object" ? { ...prevRepo } : {};
|
|
84
|
+
repoObject.type = repoObject.type ?? "git";
|
|
85
|
+
repoObject.url = patch.repositoryUrl;
|
|
86
|
+
next.repository = repoObject;
|
|
87
|
+
next.homepage = patch.homepage;
|
|
88
|
+
const prevBugs = read.data.bugs;
|
|
89
|
+
const bugsObject = prevBugs !== null && typeof prevBugs === "object" ? { ...prevBugs } : {};
|
|
90
|
+
bugsObject.url = patch.bugsUrl;
|
|
91
|
+
next.bugs = bugsObject;
|
|
92
|
+
const diffs = [];
|
|
93
|
+
if (before.repositoryUrl !== patch.repositoryUrl) {
|
|
94
|
+
diffs.push({
|
|
95
|
+
field: "repository.url",
|
|
96
|
+
before: before.repositoryUrl,
|
|
97
|
+
after: patch.repositoryUrl,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (before.homepage !== patch.homepage) {
|
|
101
|
+
diffs.push({
|
|
102
|
+
field: "homepage",
|
|
103
|
+
before: before.homepage,
|
|
104
|
+
after: patch.homepage,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (before.bugsUrl !== patch.bugsUrl) {
|
|
108
|
+
diffs.push({
|
|
109
|
+
field: "bugs.url",
|
|
110
|
+
before: before.bugsUrl,
|
|
111
|
+
after: patch.bugsUrl,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (input.dryRun === true || diffs.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
diffs,
|
|
118
|
+
next,
|
|
119
|
+
written: false,
|
|
120
|
+
path,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const indent = detectIndent(read.raw);
|
|
124
|
+
const text = stringifyKeepingTrailingNewline(next, read.raw, indent);
|
|
125
|
+
await writeText(path, text);
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
diffs,
|
|
129
|
+
next,
|
|
130
|
+
written: true,
|
|
131
|
+
path,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
4
|
+
export const PACKAGE_ROOT = resolve(here, "..", "..");
|
|
5
|
+
export const TEMPLATES_ROOT = join(PACKAGE_ROOT, "templates");
|
|
6
|
+
export const VIBEOPS_CONFIG_FILE = ".vibeops.json";
|
|
7
|
+
export const VIBEOPS_ENV_FILE = ".vibeops.env";
|
|
8
|
+
export const VIBEOPS_ENV_EXAMPLE_FILE = ".vibeops.env.example";
|
|
9
|
+
export function projectPaths(root) {
|
|
10
|
+
const abs = resolve(root);
|
|
11
|
+
return {
|
|
12
|
+
root: abs,
|
|
13
|
+
config: join(abs, VIBEOPS_CONFIG_FILE),
|
|
14
|
+
envExample: join(abs, VIBEOPS_ENV_EXAMPLE_FILE),
|
|
15
|
+
agentsMd: join(abs, "AGENTS.md"),
|
|
16
|
+
cursorRules: join(abs, ".cursor", "rules"),
|
|
17
|
+
docsProject: join(abs, "docs", "project"),
|
|
18
|
+
docsTasks: join(abs, "docs", "tasks"),
|
|
19
|
+
docsLogs: join(abs, "docs", "logs"),
|
|
20
|
+
vibeopsDir: join(abs, ".vibeops"),
|
|
21
|
+
vibeopsAgents: join(abs, ".vibeops", "agents"),
|
|
22
|
+
vibeopsPrompts: join(abs, ".vibeops", "prompts"),
|
|
23
|
+
vibeopsWorkflows: join(abs, ".vibeops", "workflows"),
|
|
24
|
+
gitignore: join(abs, ".gitignore"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
|
+
import { readTextOrNull } from "./filesystem.js";
|
|
3
|
+
import { projectPaths } from "./paths.js";
|
|
4
|
+
/**
|
|
5
|
+
* Canonical project documentation slots that `task generate` looks at.
|
|
6
|
+
* Files that don't exist are returned with `content: null` so the caller
|
|
7
|
+
* can decide whether to mention them as "missing" in the generated prompt.
|
|
8
|
+
*/
|
|
9
|
+
const PROJECT_SLOTS = [
|
|
10
|
+
{ label: "Backlog (primary)", fileName: "07-backlog.md", category: "backlog", primary: true },
|
|
11
|
+
{ label: "Overview", fileName: "00-overview.md", category: "overview" },
|
|
12
|
+
{ label: "Requirements", fileName: "01-requirements.md", category: "requirements" },
|
|
13
|
+
{ label: "MVP Scope", fileName: "02-mvp-scope.md", category: "mvp-scope" },
|
|
14
|
+
{ label: "Architecture", fileName: "03-architecture.md", category: "architecture" },
|
|
15
|
+
{ label: "Tech Stack", fileName: "04-tech-stack.md", category: "tech-stack" },
|
|
16
|
+
{ label: "Current State", fileName: "05-current-state.md", category: "current-state" },
|
|
17
|
+
{ label: "Decisions", fileName: "06-decisions.md", category: "decisions" },
|
|
18
|
+
{ label: "Env", fileName: "08-env.md", category: "env" },
|
|
19
|
+
{ label: "Deployment", fileName: "09-deployment.md", category: "deployment" },
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Fallback set of project doc filenames for repositories that pre-date the
|
|
23
|
+
* 10-file template layout (e.g. this VibeOps repo itself uses 00–05).
|
|
24
|
+
* Only consulted when a slot above is missing.
|
|
25
|
+
*/
|
|
26
|
+
const LEGACY_FALLBACKS = new Map([
|
|
27
|
+
["07-backlog.md", ["05-backlog.md"]],
|
|
28
|
+
["00-overview.md", []],
|
|
29
|
+
["03-architecture.md", ["01-architecture.md"]],
|
|
30
|
+
["04-tech-stack.md", ["02-tech-stack.md"]],
|
|
31
|
+
["05-current-state.md", ["03-current-state.md"]],
|
|
32
|
+
["06-decisions.md", ["04-decisions.md"]],
|
|
33
|
+
]);
|
|
34
|
+
function relDisplay(root, abs) {
|
|
35
|
+
const r = relative(root, abs);
|
|
36
|
+
return r.length === 0 ? "." : r;
|
|
37
|
+
}
|
|
38
|
+
async function readSlotWithFallback(cwd, spec) {
|
|
39
|
+
const paths = projectPaths(cwd);
|
|
40
|
+
const canonical = join(paths.docsProject, spec.fileName);
|
|
41
|
+
let abs = canonical;
|
|
42
|
+
let content = await readTextOrNull(abs);
|
|
43
|
+
if (content === null) {
|
|
44
|
+
for (const fallback of LEGACY_FALLBACKS.get(spec.fileName) ?? []) {
|
|
45
|
+
abs = join(paths.docsProject, fallback);
|
|
46
|
+
content = await readTextOrNull(abs);
|
|
47
|
+
if (content !== null)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (content === null)
|
|
51
|
+
abs = canonical;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
label: spec.label,
|
|
55
|
+
path: abs,
|
|
56
|
+
relativePath: relDisplay(cwd, abs),
|
|
57
|
+
content,
|
|
58
|
+
primary: spec.primary === true,
|
|
59
|
+
category: spec.category,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function collectInputDocs(inputs) {
|
|
63
|
+
const slots = [];
|
|
64
|
+
for (const spec of PROJECT_SLOTS) {
|
|
65
|
+
slots.push(await readSlotWithFallback(inputs.cwd, spec));
|
|
66
|
+
}
|
|
67
|
+
const briefContent = await readTextOrNull(inputs.briefAbs);
|
|
68
|
+
const brief = {
|
|
69
|
+
label: "Project Brief",
|
|
70
|
+
path: inputs.briefAbs,
|
|
71
|
+
relativePath: relDisplay(inputs.cwd, inputs.briefAbs),
|
|
72
|
+
content: briefContent,
|
|
73
|
+
primary: false,
|
|
74
|
+
category: "brief",
|
|
75
|
+
};
|
|
76
|
+
let from = null;
|
|
77
|
+
if (typeof inputs.fromAbs === "string" && inputs.fromAbs.length > 0) {
|
|
78
|
+
const content = await readTextOrNull(inputs.fromAbs);
|
|
79
|
+
from = {
|
|
80
|
+
label: "Custom input (--from)",
|
|
81
|
+
path: inputs.fromAbs,
|
|
82
|
+
relativePath: relDisplay(inputs.cwd, inputs.fromAbs),
|
|
83
|
+
content,
|
|
84
|
+
primary: true,
|
|
85
|
+
category: "custom",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { slots, brief, from };
|
|
89
|
+
}
|
|
90
|
+
export function presentSlots(docs) {
|
|
91
|
+
return docs.slots.filter((s) => s.content !== null);
|
|
92
|
+
}
|
|
93
|
+
export function missingSlots(docs) {
|
|
94
|
+
return docs.slots.filter((s) => s.content === null);
|
|
95
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
function renderTopList(values) {
|
|
2
|
+
if (values.length === 0)
|
|
3
|
+
return "- _(none selected)_";
|
|
4
|
+
return values.map((v) => `- ${v}`).join("\n");
|
|
5
|
+
}
|
|
6
|
+
function renderInlineList(label, values) {
|
|
7
|
+
if (values.length === 0)
|
|
8
|
+
return `- **${label}**: _(none selected)_`;
|
|
9
|
+
const head = `- **${label}**:`;
|
|
10
|
+
const items = values.map((v) => ` - ${v}`).join("\n");
|
|
11
|
+
return `${head}\n${items}`;
|
|
12
|
+
}
|
|
13
|
+
function renderBool(value) {
|
|
14
|
+
return value ? "yes" : "no";
|
|
15
|
+
}
|
|
16
|
+
function summary(brief) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
lines.push(`- **Project name**: ${brief.projectName}`);
|
|
19
|
+
lines.push(`- **One-line idea**: ${brief.oneLineIdea}`);
|
|
20
|
+
lines.push(`- **Project type**: ${brief.projectType}`);
|
|
21
|
+
lines.push(renderInlineList("Target users", brief.targetUsers));
|
|
22
|
+
lines.push(`- **Core problem**: ${brief.coreProblem}`);
|
|
23
|
+
lines.push(renderInlineList("MVP must-have features", brief.mvpFeatures));
|
|
24
|
+
lines.push(renderInlineList("Out of scope for MVP", brief.outOfScope));
|
|
25
|
+
lines.push(`- **Frontend**: ${brief.frontend}`);
|
|
26
|
+
lines.push(`- **Backend**: ${brief.backend}`);
|
|
27
|
+
lines.push(`- **Database**: ${brief.database}`);
|
|
28
|
+
lines.push(`- **ORM / DB layer**: ${brief.dbLayer}`);
|
|
29
|
+
lines.push(`- **Package manager**: ${brief.packageManager}`);
|
|
30
|
+
lines.push(renderInlineList("Deployment target", brief.deploymentTargets));
|
|
31
|
+
lines.push(renderInlineList("Auth requirement", brief.authRequirements));
|
|
32
|
+
lines.push(renderInlineList("External integrations", brief.integrations));
|
|
33
|
+
lines.push(`- **Use Notion dashboard sync**: ${renderBool(brief.useNotion)}`);
|
|
34
|
+
lines.push(`- **Use Git task branch workflow**: ${renderBool(brief.useGitWorkflow)}`);
|
|
35
|
+
lines.push(`- **Agent workflow level**: ${brief.agentWorkflowLevel}`);
|
|
36
|
+
lines.push(renderInlineList("Risk areas", brief.risks));
|
|
37
|
+
lines.push(`- **Success criteria**: ${brief.successCriteria}`);
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
export function buildPlanPrompt(inputs) {
|
|
41
|
+
const { brief, meta, briefRelativePath } = inputs;
|
|
42
|
+
const assumptions = meta.assumptions.length > 0
|
|
43
|
+
? meta.assumptions.map((a) => `- ${a}`).join("\n")
|
|
44
|
+
: "- _(none recorded)_";
|
|
45
|
+
return `# VibeOps Plan Prompt — Cursor Planner Agent
|
|
46
|
+
|
|
47
|
+
> This file was produced by \`vibeops plan\`. Open a new chat in **Cursor** and paste the full contents of this file. VibeOps does not call LLMs directly — the Planner Agent receives this input and fills in \`docs/project/*\` plus the initial backlog.
|
|
48
|
+
|
|
49
|
+
- Brief location: \`${briefRelativePath}\`
|
|
50
|
+
- VibeOps version: ${meta.vibeopsVersion}
|
|
51
|
+
- Generated: ${meta.generatedAt}
|
|
52
|
+
- Source: ${meta.source} · schemaVersion: ${meta.schemaVersion}
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Role: Planner Agent
|
|
57
|
+
|
|
58
|
+
Follow the definition in \`.vibeops/agents/planner.md\`. You do not write application code. You produce:
|
|
59
|
+
|
|
60
|
+
1. Updates to \`docs/project/*\` — vision / requirements / scope / architecture draft / tech stack / decisions / backlog / environment / deployment notes.
|
|
61
|
+
2. An initial backlog of \`docs/tasks/TASK-NNN-*.md\` files (3-6 to start). Each TASK contains the sections Status · MVP Phase · Goal · Scope · Out of Scope · Acceptance Criteria · Test Plan · Result · Test Result.
|
|
62
|
+
|
|
63
|
+
## Hard rules (failure if violated)
|
|
64
|
+
|
|
65
|
+
- Do not produce application code. The deliverable for this round is limited to \`docs/**\`.
|
|
66
|
+
- Do not touch VibeOps' own config (\`.vibeops/\`, \`.vibeops.json\`, \`templates/\`).
|
|
67
|
+
- Source-of-truth rules: \`docs/tasks/*.md\` = AI execution baseline, \`docs/project/*.md\` = design / current-state baseline, Git commits/branches = change history, Notion = human dashboard (summary / status / priority / docs path only), chat is never a baseline.
|
|
68
|
+
- One TASK, one focus. Surface inter-TASK dependencies in the body.
|
|
69
|
+
- Never hide assumptions. Record them in both the docs/project bodies and the closing "Assumptions" section.
|
|
70
|
+
- Notion / Git workflow / agent workflow level follow the ProjectBrief values below as-is. Do not change them on a whim.
|
|
71
|
+
|
|
72
|
+
## ProjectBrief (user answers)
|
|
73
|
+
|
|
74
|
+
${summary(brief)}
|
|
75
|
+
|
|
76
|
+
### Assumptions inherited from the brief
|
|
77
|
+
|
|
78
|
+
${assumptions}
|
|
79
|
+
|
|
80
|
+
## Output format
|
|
81
|
+
|
|
82
|
+
Produce the response in this exact order:
|
|
83
|
+
|
|
84
|
+
1. **Plan Summary** — 5 to 8 bullets capturing the direction derived from the ProjectBrief (audience, scope, tech selections, key risks).
|
|
85
|
+
2. **docs/project/\\*** — emit the 8 files below, each in its own fenced code block. The first line of each block is \`<!-- file: docs/project/XX-name.md -->\`. (\`03-architecture\` and \`05-current-state\` are not produced in this round.)
|
|
86
|
+
- \`docs/project/00-overview.md\`
|
|
87
|
+
- \`docs/project/01-requirements.md\`
|
|
88
|
+
- \`docs/project/02-mvp-scope.md\`
|
|
89
|
+
- \`docs/project/04-tech-stack.md\`
|
|
90
|
+
- \`docs/project/06-decisions.md\`
|
|
91
|
+
- \`docs/project/07-backlog.md\`
|
|
92
|
+
- \`docs/project/08-env.md\`
|
|
93
|
+
- \`docs/project/09-deployment.md\`
|
|
94
|
+
3. **docs/tasks/TASK-NNN-\\*** — 3 to 6 initial backlog items. Each in a fenced block beginning with \`<!-- file: docs/tasks/TASK-NNN-slug.md -->\`.
|
|
95
|
+
4. **Changed file list** — the full list of files produced above.
|
|
96
|
+
5. **Assumptions** — decisions the user must reconfirm (\`(none)\` if there are none).
|
|
97
|
+
|
|
98
|
+
## Field mapping (brief field → docs file)
|
|
99
|
+
|
|
100
|
+
- \`00-overview.md\` ← projectName, oneLineIdea, projectType, targetUsers, coreProblem, successCriteria
|
|
101
|
+
- \`01-requirements.md\` ← mvpFeatures, authRequirements, integrations, targetUsers
|
|
102
|
+
- \`02-mvp-scope.md\` ← mvpFeatures (IN), outOfScope (OUT), successCriteria
|
|
103
|
+
- \`04-tech-stack.md\` ← frontend, backend, database, dbLayer, packageManager
|
|
104
|
+
- \`06-decisions.md\` ← useNotion, useGitWorkflow, agentWorkflowLevel, packageManager, plus any auto-derived decisions.
|
|
105
|
+
- \`07-backlog.md\` ← decompose mvpFeatures into TASKs (1-2 per feature + 1 setup). Include priority and definition of done.
|
|
106
|
+
- \`08-env.md\` ← env variables per integration (e.g. OpenAI → OPENAI_API_KEY) and their purpose.
|
|
107
|
+
- \`09-deployment.md\` ← deployment notes per deploymentTargets. If only "Not sure" is selected, state that explicitly and mark the decision as pending.
|
|
108
|
+
|
|
109
|
+
## Notion / Git / Agent workflow handling
|
|
110
|
+
|
|
111
|
+
- Use Notion dashboard sync: ${renderBool(brief.useNotion)} → record in \`06-decisions.md\`. ${brief.useNotion ? "Decision: VibeOps will sync Notion DB metadata." : "Decision: no Notion sync (revisit in a future TASK if needed)."}
|
|
112
|
+
- Use Git task branch workflow: ${renderBool(brief.useGitWorkflow)} → ${brief.useGitWorkflow ? "TASK lifecycle assumes the task/TASK-NNN-slug branch model." : "Record that the Git task-branch model is not used (linear workflow)."}
|
|
113
|
+
- Agent workflow level: \`${brief.agentWorkflowLevel}\` → fix the agent line-up in \`06-decisions.md\`.
|
|
114
|
+
|
|
115
|
+
## Risk areas → docs
|
|
116
|
+
|
|
117
|
+
${renderTopList(brief.risks)}
|
|
118
|
+
|
|
119
|
+
Record each risk in \`07-backlog.md\` or in the Risks section of the corresponding TASK. Operational risks such as "Authentication/security" or "Browser automation reliability" become candidates for their own TASK.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
Apply the rules above and produce the response. After you respond, a human reviews via \`git diff\` and commits.
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export const TASK_GENERATE_SCHEMA_VERSION = 1;
|
|
2
|
+
export const REQUIRED_TASK_SECTIONS = [
|
|
3
|
+
"Status",
|
|
4
|
+
"MVP Phase",
|
|
5
|
+
"Goal",
|
|
6
|
+
"Background",
|
|
7
|
+
"Scope",
|
|
8
|
+
"Out of Scope",
|
|
9
|
+
"Acceptance Criteria",
|
|
10
|
+
"Files to Inspect First",
|
|
11
|
+
"Expected Files to Change",
|
|
12
|
+
"Risks",
|
|
13
|
+
"Test Plan",
|
|
14
|
+
"Rollback Plan",
|
|
15
|
+
"Git Context",
|
|
16
|
+
"Notion Page",
|
|
17
|
+
"Implementation Plan",
|
|
18
|
+
"Result",
|
|
19
|
+
"Test Result",
|
|
20
|
+
"Review Notes",
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Convert an arbitrary string to a TASK-filename slug.
|
|
24
|
+
* - lowercase
|
|
25
|
+
* - ASCII-only (non-ASCII letters dropped)
|
|
26
|
+
* - non-alphanum collapsed to `-`
|
|
27
|
+
* - leading/trailing `-` removed, repeated `-` compressed
|
|
28
|
+
* - empty result → `task`
|
|
29
|
+
*/
|
|
30
|
+
export function slugify(input, fallback = "task") {
|
|
31
|
+
const cleaned = input
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.normalize("NFKD")
|
|
34
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
35
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
36
|
+
.replace(/^-+|-+$/g, "")
|
|
37
|
+
.replace(/-+/g, "-");
|
|
38
|
+
return cleaned.length > 0 ? cleaned : fallback;
|
|
39
|
+
}
|
|
40
|
+
function header(inputs) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
lines.push("# VibeOps Task-Generation Prompt — Cursor Planner Agent");
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push("> This file was produced by `vibeops task generate`. Open a new chat in **Cursor** and paste the full contents of this file. VibeOps does not call LLM, Cursor CLI, Notion, or GitHub APIs directly — the **Planner Agent** receiving this prompt creates the `docs/tasks/TASK-*.md` files.");
|
|
45
|
+
lines.push("");
|
|
46
|
+
lines.push(`- VibeOps version: ${inputs.vibeopsVersion}`);
|
|
47
|
+
lines.push(`- Generated: ${inputs.generatedAt}`);
|
|
48
|
+
lines.push(`- Generated by: \`vibeops task generate\``);
|
|
49
|
+
lines.push(`- Prompt location: \`${inputs.outputRelative}\``);
|
|
50
|
+
lines.push(`- Schema version: ${TASK_GENERATE_SCHEMA_VERSION}`);
|
|
51
|
+
lines.push("");
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
function role() {
|
|
55
|
+
return `## Role: Planner Agent
|
|
56
|
+
|
|
57
|
+
Follow the definition in \`.vibeops/agents/planner.md\` if it is installed. You **do not write application code**. You produce \`docs/tasks/TASK-NNN-slug.md\` files. Each TASK must be small enough for a single Builder Agent to finish in one Cursor session.`;
|
|
58
|
+
}
|
|
59
|
+
function hardRules() {
|
|
60
|
+
return `## Hard rules (failure if violated)
|
|
61
|
+
|
|
62
|
+
- Never produce application code (\`src/**\`, \`lib/**\`, \`app/**\`, tests, build scripts, etc.). The deliverable for this round is \`docs/tasks/TASK-NNN-slug.md\` files only.
|
|
63
|
+
- Do not touch VibeOps' own config (\`.vibeops/\`, \`.vibeops.json\`, \`templates/\`).
|
|
64
|
+
- Never call LLM APIs, Cursor CLI, Notion APIs, or GitHub APIs directly. You only produce markdown.
|
|
65
|
+
- Source-of-truth rules:
|
|
66
|
+
- **\`docs/tasks/*.md\` = AI execution source of truth** (Builder/Reviewer/Tester agents only read these files).
|
|
67
|
+
- **\`docs/project/*.md\` = design + current-state baseline** (the input the Planner reads for this round).
|
|
68
|
+
- **Git commits/branches = change history**.
|
|
69
|
+
- **Notion = human dashboard** (exposes summary / status / priority / docs path only — never execution details).
|
|
70
|
+
- Do not trust chat memory.
|
|
71
|
+
- One TASK, one focus. Surface inter-TASK dependencies in the body (e.g. "TASK-014 depends on TASK-013").
|
|
72
|
+
- Never hide assumptions. Record them in the closing "Assumptions" section of the response.`;
|
|
73
|
+
}
|
|
74
|
+
function inventoryRow(slot) {
|
|
75
|
+
const flag = slot.content === null ? "·" : "✓";
|
|
76
|
+
const note = slot.content === null ? " _(missing)_" : "";
|
|
77
|
+
const primaryTag = slot.primary ? " **(primary)**" : "";
|
|
78
|
+
return `- [${flag}] \`${slot.relativePath}\` — ${slot.label}${primaryTag}${note}`;
|
|
79
|
+
}
|
|
80
|
+
function inventory(docs) {
|
|
81
|
+
const lines = ["## Input documents (for this round)"];
|
|
82
|
+
lines.push("");
|
|
83
|
+
if (docs.from)
|
|
84
|
+
lines.push(inventoryRow(docs.from));
|
|
85
|
+
for (const slot of docs.slots)
|
|
86
|
+
lines.push(inventoryRow(slot));
|
|
87
|
+
lines.push(inventoryRow(docs.brief));
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push("> `[✓]` = file read from disk. `[·]` = missing or empty slot — its content is not inlined below. Do not over-guess for missing slots; record them in \"Assumptions\".");
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
function objective(inputs) {
|
|
93
|
+
const lines = ["## Objective"];
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push(`Read the inputs above and produce roughly **${inputs.count} new TASK files**. The first TASK number is \`${inputs.nextTaskId}\` (the next number after the highest existing \`docs/tasks/TASK-*.md\`). Skip any number that already exists on disk.`);
|
|
96
|
+
if (typeof inputs.phase === "string" && inputs.phase.length > 0) {
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push(`- **Phase filter**: this round only produces TASKs whose phase is \`${inputs.phase}\`. Even if you see work for other phases, leave it for a separate round.`);
|
|
99
|
+
}
|
|
100
|
+
if (inputs.countWarning) {
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push(`- ⚠️ A recommended count of ${inputs.count} is on the high side. Make sure each TASK really does fit a single Cursor session, and lower the count if anything is vague.`);
|
|
103
|
+
}
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push("Each TASK must satisfy:");
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push("- **Small, verifiable scope**. One Builder finishes it in one Cursor session.");
|
|
108
|
+
lines.push("- **Self-contained**. A reader can determine the files to change and the Acceptance Criteria from the TASK body alone.");
|
|
109
|
+
lines.push("- **Explicit dependencies**. If TASK depends on another, write \"depends on TASK-NNN\" in Background.");
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
function fileFormat(inputs) {
|
|
113
|
+
const sections = REQUIRED_TASK_SECTIONS.map((s) => ` - \`## ${s}\``).join("\n");
|
|
114
|
+
return `## Output format
|
|
115
|
+
|
|
116
|
+
Produce the response in this exact order:
|
|
117
|
+
|
|
118
|
+
1. **Plan summary** — 5 to 8 bullets. Priorities / decomposition derived from the inputs.
|
|
119
|
+
2. **TASK files** — each in its own fenced code block. The first line of each block must be \`<!-- file: docs/tasks/TASK-NNN-slug.md -->\` (an HTML comment) marking the path. Numbers increase sequentially from \`${inputs.nextTaskId}\`. Slugs are lowercase / ASCII / hyphen-separated (e.g. \`docs-and-readme\`).
|
|
120
|
+
3. **Changed file list** — every \`docs/tasks/TASK-NNN-*.md\` path produced above.
|
|
121
|
+
4. **Generated TASK summary** — table or bullet list: \`TASK-NNN · title · MVP Phase · one-line summary\`.
|
|
122
|
+
5. **Assumptions** — items a human needs to reconfirm (use \`(none)\` if there are none).
|
|
123
|
+
|
|
124
|
+
### Required sections per TASK file (same order, same heading text)
|
|
125
|
+
|
|
126
|
+
Each TASK markdown begins with \`# ${inputs.nextTaskId} · <one-line title>\` and includes **all** of the following \`##\` sections:
|
|
127
|
+
|
|
128
|
+
${sections}
|
|
129
|
+
|
|
130
|
+
Additional rules:
|
|
131
|
+
|
|
132
|
+
- The body of \`## Status\` is the single word \`planned\`.
|
|
133
|
+
- The body of \`## MVP Phase\` follows \`${typeof inputs.phase === "string" && inputs.phase.length > 0 ? inputs.phase : "MVP <n> · <phase name>"}\` (or \`(unassigned)\` when unknown).
|
|
134
|
+
- The body of \`## Git Context\` is \`(populated by \\\`vibeops task start TASK-NNN\\\`)\`.
|
|
135
|
+
- The body of \`## Notion Page\` is \`(populated by \\\`vibeops notion sync\\\`)\`.
|
|
136
|
+
- The body of \`## Result\` / \`## Test Result\` / \`## Review Notes\` is \`(not yet)\`.`;
|
|
137
|
+
}
|
|
138
|
+
function workflowReminders() {
|
|
139
|
+
return `## Workflow reminders
|
|
140
|
+
|
|
141
|
+
- **VibeOps source of truth = \`docs/tasks/\` + \`docs/project/\` + Git.** Notion is a human dashboard and is never used for AI execution.
|
|
142
|
+
- Once produced, each TASK must be ready for \`vibeops task start TASK-NNN\`. Keep slugs clean and numbers conflict-free.
|
|
143
|
+
- Never produce application code — a separate Builder Agent does that in a different round.
|
|
144
|
+
- Separate ambiguity into \`Risks\` (known unknowns) and \`Assumptions\` (need confirmation).`;
|
|
145
|
+
}
|
|
146
|
+
function fileSection(doc, fenceLang = "") {
|
|
147
|
+
if (doc.content === null) {
|
|
148
|
+
return `### \`${doc.relativePath}\` _(missing)_\n\n_(file not found — the Planner Agent must record this in Assumptions.)_`;
|
|
149
|
+
}
|
|
150
|
+
const fence = "```";
|
|
151
|
+
const lang = fenceLang.length > 0 ? fenceLang : "markdown";
|
|
152
|
+
return `### \`${doc.relativePath}\`\n\n${fence}${lang}\n${doc.content.trimEnd()}\n${fence}`;
|
|
153
|
+
}
|
|
154
|
+
function inlineInputs(docs) {
|
|
155
|
+
const parts = ["## Inline inputs"];
|
|
156
|
+
parts.push("");
|
|
157
|
+
parts.push("The code blocks below are the on-disk snapshot at prompt generation time. Cursor may open additional context, but **start from these**.");
|
|
158
|
+
parts.push("");
|
|
159
|
+
if (docs.from)
|
|
160
|
+
parts.push(fileSection(docs.from));
|
|
161
|
+
for (const slot of docs.slots)
|
|
162
|
+
parts.push(fileSection(slot));
|
|
163
|
+
parts.push(fileSection(docs.brief));
|
|
164
|
+
return parts.join("\n\n");
|
|
165
|
+
}
|
|
166
|
+
function footer() {
|
|
167
|
+
return `---
|
|
168
|
+
|
|
169
|
+
Apply the rules above and produce the response. After you respond, a human reviews the generated \`docs/tasks/TASK-*.md\` files via \`git diff\` and commits them, then starts the first TASK with \`vibeops task start TASK-NNN\`.`;
|
|
170
|
+
}
|
|
171
|
+
export function buildTaskGeneratePrompt(inputs) {
|
|
172
|
+
return [
|
|
173
|
+
header(inputs),
|
|
174
|
+
role(),
|
|
175
|
+
hardRules(),
|
|
176
|
+
inventory(inputs.docs),
|
|
177
|
+
objective(inputs),
|
|
178
|
+
fileFormat(inputs),
|
|
179
|
+
workflowReminders(),
|
|
180
|
+
inlineInputs(inputs.docs),
|
|
181
|
+
footer(),
|
|
182
|
+
].join("\n\n");
|
|
183
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { findAgent, listAgents } from "../agent/loader.js";
|
|
2
|
+
import { buildPrompt } from "../agent/prompt.js";
|
|
3
|
+
import { readConfig } from "./config.js";
|
|
4
|
+
import { readText } from "./filesystem.js";
|
|
5
|
+
import { readTaskFile } from "./task.js";
|
|
6
|
+
export async function buildTaskPromptString(inputs) {
|
|
7
|
+
const agent = await findAgent(inputs.agentsDir, inputs.agentName);
|
|
8
|
+
if (!agent) {
|
|
9
|
+
const available = (await listAgents(inputs.agentsDir)).map((a) => a.meta.name);
|
|
10
|
+
return { ok: false, reason: "agent-not-found", available };
|
|
11
|
+
}
|
|
12
|
+
const config = await readConfig(inputs.projectRoot);
|
|
13
|
+
const meta = await readTaskFile(inputs.taskFilePath);
|
|
14
|
+
const body = await readText(inputs.taskFilePath);
|
|
15
|
+
const prompt = await buildPrompt({
|
|
16
|
+
agent,
|
|
17
|
+
config,
|
|
18
|
+
task: { meta, body },
|
|
19
|
+
projectRoot: inputs.projectRoot,
|
|
20
|
+
contextPaths: inputs.contextPaths ?? [],
|
|
21
|
+
});
|
|
22
|
+
return { ok: true, prompt };
|
|
23
|
+
}
|