@goodtek/vibeops 0.7.22 → 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.
Files changed (121) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +62 -382
  3. package/dist/bootstrap/installer.js +4 -16
  4. package/dist/cli.js +26 -160
  5. package/dist/commands/init.js +3 -3
  6. package/dist/commands/status.js +145 -8
  7. package/dist/commands/task-add.js +64 -164
  8. package/dist/commands/task-done.js +117 -74
  9. package/dist/lib/config.js +2 -118
  10. package/dist/lib/git.js +1 -1
  11. package/dist/lib/paths.js +7 -3
  12. package/dist/lib/plan-llm-openai.js +0 -63
  13. package/dist/lib/resolve-task.js +34 -0
  14. package/dist/lib/task-add-llm.js +46 -233
  15. package/dist/lib/task-context.js +68 -0
  16. package/dist/lib/task-done-llm.js +148 -0
  17. package/dist/lib/task-merge.js +54 -159
  18. package/dist/lib/task-scaffold.js +39 -43
  19. package/dist/lib/task-start.js +72 -0
  20. package/dist/lib/task-summary.js +12 -239
  21. package/dist/lib/task.js +15 -24
  22. package/dist/types/config.js +0 -16
  23. package/package.json +3 -5
  24. package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  25. package/templates/.cursor/rules/02-git-safety.mdc +11 -0
  26. package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
  27. package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
  28. package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
  29. package/templates/AGENTS.md +26 -87
  30. package/templates/docs/logs/README.md +4 -35
  31. package/templates/docs/project/03-architecture.md +10 -29
  32. package/templates/docs/project/05-current-state.md +7 -27
  33. package/templates/docs/project/06-decisions.md +6 -17
  34. package/templates/docs/tasks/TASK-000-template.md +9 -44
  35. package/dist/agent/loader.js +0 -71
  36. package/dist/agent/prompt.js +0 -66
  37. package/dist/commands/agent-list.js +0 -32
  38. package/dist/commands/agent-prompt.js +0 -59
  39. package/dist/commands/agent-show.js +0 -26
  40. package/dist/commands/done.js +0 -160
  41. package/dist/commands/github-init.js +0 -554
  42. package/dist/commands/github-status.js +0 -164
  43. package/dist/commands/next.js +0 -283
  44. package/dist/commands/notion-init.js +0 -764
  45. package/dist/commands/notion-sync.js +0 -405
  46. package/dist/commands/notion-test.js +0 -595
  47. package/dist/commands/plan.js +0 -146
  48. package/dist/commands/rollback.js +0 -126
  49. package/dist/commands/start.js +0 -129
  50. package/dist/commands/task-check.js +0 -155
  51. package/dist/commands/task-generate.js +0 -206
  52. package/dist/commands/task-pull.js +0 -277
  53. package/dist/commands/task-rollback.js +0 -174
  54. package/dist/commands/task-start.js +0 -102
  55. package/dist/commands/task-status.js +0 -122
  56. package/dist/lib/brief.js +0 -356
  57. package/dist/lib/cursor-task-prompt.js +0 -60
  58. package/dist/lib/done-follow-up.js +0 -308
  59. package/dist/lib/github-cli.js +0 -247
  60. package/dist/lib/guide-state.js +0 -69
  61. package/dist/lib/mvp-artifacts.js +0 -204
  62. package/dist/lib/mvp-constants.js +0 -15
  63. package/dist/lib/mvp-diff-summary.js +0 -43
  64. package/dist/lib/mvp-done-summary.js +0 -335
  65. package/dist/lib/notion-client.js +0 -459
  66. package/dist/lib/notion-discovery.js +0 -671
  67. package/dist/lib/notion-env.js +0 -140
  68. package/dist/lib/notion-mappers.js +0 -148
  69. package/dist/lib/notion-schema.js +0 -272
  70. package/dist/lib/notion-sync.js +0 -337
  71. package/dist/lib/notion-target.js +0 -247
  72. package/dist/lib/package-json.js +0 -133
  73. package/dist/lib/plan-apply-planner.js +0 -131
  74. package/dist/lib/plan-codex-pkce.js +0 -234
  75. package/dist/lib/plan-llm-models.js +0 -237
  76. package/dist/lib/plan-llm-normalize.js +0 -146
  77. package/dist/lib/plan-llm-session.js +0 -682
  78. package/dist/lib/plan-post-apply-setup.js +0 -116
  79. package/dist/lib/project-docs.js +0 -95
  80. package/dist/lib/prompt-builder.js +0 -135
  81. package/dist/lib/resolve-command-task.js +0 -40
  82. package/dist/lib/task-add-build-prompt.js +0 -39
  83. package/dist/lib/task-add-cursor.js +0 -81
  84. package/dist/lib/task-generator.js +0 -183
  85. package/dist/lib/task-prompt.js +0 -23
  86. package/dist/lib/task-pull.js +0 -354
  87. package/dist/lib/task-status-brief.js +0 -132
  88. package/dist/lib/workflow-guide.js +0 -485
  89. package/dist/status/collector.js +0 -103
  90. package/dist/status/format.js +0 -181
  91. package/dist/types/brief.js +0 -126
  92. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  93. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  94. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  95. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  96. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  97. package/templates/.vibeops/agents/architect.md +0 -47
  98. package/templates/.vibeops/agents/builder.md +0 -38
  99. package/templates/.vibeops/agents/docs.md +0 -54
  100. package/templates/.vibeops/agents/orchestrator.md +0 -40
  101. package/templates/.vibeops/agents/planner.md +0 -60
  102. package/templates/.vibeops/agents/recovery.md +0 -49
  103. package/templates/.vibeops/agents/reviewer.md +0 -47
  104. package/templates/.vibeops/agents/tester.md +0 -43
  105. package/templates/.vibeops/prompts/create-plan.md +0 -33
  106. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  107. package/templates/.vibeops/prompts/implement-task.md +0 -39
  108. package/templates/.vibeops/prompts/review-task.md +0 -34
  109. package/templates/.vibeops/prompts/rollback.md +0 -32
  110. package/templates/.vibeops/prompts/start-project.md +0 -39
  111. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  112. package/templates/.vibeops/workflows/project-start.md +0 -73
  113. package/templates/.vibeops/workflows/rollback.md +0 -45
  114. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  115. package/templates/docs/project/00-overview.md +0 -27
  116. package/templates/docs/project/01-requirements.md +0 -30
  117. package/templates/docs/project/02-mvp-scope.md +0 -36
  118. package/templates/docs/project/04-tech-stack.md +0 -29
  119. package/templates/docs/project/07-backlog.md +0 -23
  120. package/templates/docs/project/08-env.md +0 -29
  121. package/templates/docs/project/09-deployment.md +0 -28
@@ -1,206 +0,0 @@
1
- import { isAbsolute, join, relative, resolve } from "node:path";
2
- import { pathExists, writeText } from "../lib/filesystem.js";
3
- import { bold, cyan, dim, log, yellow } from "../lib/logger.js";
4
- import { projectPaths } from "../lib/paths.js";
5
- import { collectInputDocs, } from "../lib/project-docs.js";
6
- import { formatTaskId, nextTaskNumber } from "../lib/task.js";
7
- import { buildTaskGeneratePrompt, } from "../lib/task-generator.js";
8
- import { planScaffoldEntries, renderScaffoldMarkdown, writeScaffoldFiles, } from "../lib/task-scaffold.js";
9
- import { VERSION } from "../version.js";
10
- const DEFAULT_PROMPT_REL = ".vibeops/generated/task-generate-prompt.md";
11
- const DEFAULT_COUNT = 8;
12
- const COUNT_SOFT_CAP = 20;
13
- const BRIEF_REL = ".vibeops/brief/project-brief.md";
14
- function toAbsolute(root, candidate) {
15
- return isAbsolute(candidate) ? candidate : resolve(root, candidate);
16
- }
17
- function relDisplay(root, abs) {
18
- const r = relative(root, abs);
19
- return r.length === 0 ? "." : r;
20
- }
21
- function parseCount(raw) {
22
- if (raw === undefined)
23
- return { count: DEFAULT_COUNT, warning: false };
24
- const n = typeof raw === "number" ? raw : Number.parseInt(String(raw).trim(), 10);
25
- if (!Number.isFinite(n) || n <= 0) {
26
- return {
27
- count: DEFAULT_COUNT,
28
- warning: false,
29
- error: `--count must be a positive integer (got: "${String(raw)}"). Falling back to default ${DEFAULT_COUNT}.`,
30
- };
31
- }
32
- return { count: Math.floor(n), warning: n > COUNT_SOFT_CAP };
33
- }
34
- function presentInventory(docs) {
35
- const present = [];
36
- const missing = [];
37
- if (docs.from)
38
- (docs.from.content !== null ? present : missing).push(docs.from);
39
- for (const slot of docs.slots)
40
- (slot.content !== null ? present : missing).push(slot);
41
- if (docs.brief.content !== null)
42
- present.push(docs.brief);
43
- else
44
- missing.push(docs.brief);
45
- return { present, missing };
46
- }
47
- export async function taskGenerateCommand(options = {}) {
48
- const cwd = resolve(options.cwd ?? process.cwd());
49
- const paths = projectPaths(cwd);
50
- const dryRun = options.dryRun === true;
51
- const scaffold = options.scaffold === true;
52
- const briefAbs = join(cwd, BRIEF_REL);
53
- const parsedCount = parseCount(options.count);
54
- if (parsedCount.error)
55
- log.warn(parsedCount.error);
56
- const count = parsedCount.count;
57
- const countWarning = parsedCount.warning;
58
- if (countWarning) {
59
- log.warn(`--count ${count} is large (soft cap ${COUNT_SOFT_CAP}). Continuing, but the Planner Agent may push back.`);
60
- }
61
- const fromAbs = options.from ? toAbsolute(cwd, options.from) : undefined;
62
- if (typeof fromAbs === "string" && !(await pathExists(fromAbs))) {
63
- log.error(`--from path not found: ${fromAbs}`);
64
- log.info(`If you meant a relative path, run from the project root or pass --cwd <path>.`);
65
- process.exitCode = 1;
66
- return;
67
- }
68
- log.info(bold(scaffold ? "vibeops task generate --scaffold" : "vibeops task generate"));
69
- log.info(` ${dim("cwd")} ${cwd}`);
70
- log.info(` ${dim("mode")} ${scaffold ? "scaffold (write skeleton TASK files)" : "prompt (build Cursor prompt)"}`);
71
- if (options.phase)
72
- log.info(` ${dim("phase")} ${options.phase}`);
73
- log.info(` ${dim("count")} ${count}${countWarning ? yellow(" (above soft cap)") : ""}`);
74
- if (fromAbs)
75
- log.info(` ${dim("--from")} ${relDisplay(cwd, fromAbs)}`);
76
- if (dryRun)
77
- log.info(` ${dim("dry-run")} on (no file writes)`);
78
- log.blank();
79
- if (scaffold) {
80
- await runScaffold({
81
- tasksDir: paths.docsTasks,
82
- cwd,
83
- count,
84
- phase: options.phase,
85
- dryRun,
86
- });
87
- return;
88
- }
89
- await runPromptMode({
90
- cwd,
91
- paths,
92
- options,
93
- fromAbs,
94
- briefAbs,
95
- count,
96
- countWarning,
97
- dryRun,
98
- });
99
- }
100
- async function runPromptMode(inputs) {
101
- const { cwd, paths, options, fromAbs, briefAbs, count, countWarning, dryRun } = inputs;
102
- const promptAbs = typeof options.output === "string"
103
- ? toAbsolute(cwd, options.output)
104
- : join(cwd, DEFAULT_PROMPT_REL);
105
- const docs = await collectInputDocs({ cwd, fromPath: options.from, fromAbs, briefAbs });
106
- const { present, missing } = presentInventory(docs);
107
- log.info(bold("Input documents"));
108
- for (const s of present)
109
- log.info(` ${cyan("✓")} ${relDisplay(cwd, s.path)} ${dim(`(${s.label})`)}`);
110
- for (const s of missing)
111
- log.info(` ${dim("·")} ${relDisplay(cwd, s.path)} ${dim(`(${s.label} — missing)`)}`);
112
- log.blank();
113
- const nextNum = await nextTaskNumber(paths.docsTasks);
114
- const nextId = formatTaskId(nextNum);
115
- log.info(bold("Plan"));
116
- log.info(` ${dim("next TASK id")} ${nextId}`);
117
- log.info(` ${dim("prompt output")} ${relDisplay(cwd, promptAbs)}`);
118
- log.blank();
119
- if (dryRun) {
120
- log.info(bold("dry-run — would perform:"));
121
- log.info(` · read ${present.length} input doc(s) (skipped ${missing.length} missing slot${missing.length === 1 ? "" : "s"})`);
122
- log.info(` · build Cursor prompt for ~${count} TASK starting at ${nextId}`);
123
- log.info(` · write prompt to ${relDisplay(cwd, promptAbs)}`);
124
- log.info(` · no LLM / Cursor / Notion / GitHub / Git call`);
125
- log.blank();
126
- log.info(dim("no files were written."));
127
- return;
128
- }
129
- const promptInputs = {
130
- cwd,
131
- vibeopsVersion: VERSION,
132
- generatedAt: new Date().toISOString(),
133
- nextTaskId: nextId,
134
- count,
135
- outputPath: promptAbs,
136
- outputRelative: relDisplay(cwd, promptAbs),
137
- docs,
138
- countWarning,
139
- ...(typeof options.phase === "string" && options.phase.length > 0
140
- ? { phase: options.phase }
141
- : {}),
142
- };
143
- const md = buildTaskGeneratePrompt(promptInputs);
144
- await writeText(promptAbs, md);
145
- log.ok(`Cursor task-generate prompt: ${relDisplay(cwd, promptAbs)}`);
146
- log.blank();
147
- log.info(bold("Next steps"));
148
- log.info(` 1) Open a new Cursor chat and paste the full contents of ${cyan(relDisplay(cwd, promptAbs))}.`);
149
- log.info(` 2) Review the Planner Agent's \`docs/tasks/TASK-NNN-*.md\` files with \`git diff\` and commit.`);
150
- log.info(` 3) Start the first new TASK with ${cyan(`vibeops task start ${nextId}`)}.`);
151
- if (missing.length > 0) {
152
- log.blank();
153
- log.info(`${yellow("!")} ${missing.length} input slot(s) are missing on disk. The Planner Agent will see them as "missing"; decide whether to fill them this round.`);
154
- }
155
- }
156
- async function runScaffold(inputs) {
157
- const plan = await planScaffoldEntries({
158
- tasksDir: inputs.tasksDir,
159
- count: inputs.count,
160
- ...(typeof inputs.phase === "string" && inputs.phase.length > 0
161
- ? { phase: inputs.phase }
162
- : {}),
163
- });
164
- log.info(bold("Scaffold plan"));
165
- log.info(` ${dim("start at")} ${formatTaskId(plan.startNumber)}`);
166
- log.info(` ${dim("count")} ${plan.entries.length}`);
167
- log.blank();
168
- log.info(bold("Files to create"));
169
- for (const e of plan.entries) {
170
- log.info(` · ${relDisplay(inputs.cwd, e.absPath)} ${dim(`(${e.phase})`)}`);
171
- }
172
- log.blank();
173
- if (inputs.dryRun) {
174
- log.info(dim("dry-run — no files were written."));
175
- log.info(`Skeleton preview (first entry, ${plan.entries[0]?.id ?? "TASK-???"}):`);
176
- log.blank();
177
- log.info(dim("─".repeat(60)));
178
- log.raw(renderScaffoldMarkdown(plan.entries[0] ?? fallbackPreviewEntry(plan.startNumber, inputs.phase)));
179
- log.info(dim("─".repeat(60)));
180
- return;
181
- }
182
- const written = await writeScaffoldFiles(plan);
183
- for (const abs of written)
184
- log.ok(`created ${relDisplay(inputs.cwd, abs)}`);
185
- if (written.length < plan.entries.length) {
186
- const skipped = plan.entries.length - written.length;
187
- log.warn(`${skipped} planned file(s) already existed and were skipped (won't overwrite).`);
188
- }
189
- log.blank();
190
- log.info(bold("Next steps"));
191
- log.info(` 1) Fill the empty sections of each scaffolded TASK file (in Cursor, via the Planner or Architect Agent).`);
192
- log.info(` 2) After filling, run \`git add docs/tasks/ && git commit\`.`);
193
- log.info(` 3) Start the first TASK with ${cyan(`vibeops task start ${plan.entries[0]?.id ?? "TASK-???"}`)}.`);
194
- }
195
- function fallbackPreviewEntry(start, phase) {
196
- const id = formatTaskId(start);
197
- return {
198
- id,
199
- number: start,
200
- slug: "planned-task",
201
- title: "(scaffolded TASK — fill in)",
202
- fileName: `${id}-planned-task.md`,
203
- absPath: `${id}-planned-task.md`,
204
- phase: phase ?? "(unassigned)",
205
- };
206
- }
@@ -1,277 +0,0 @@
1
- import { resolve } from "node:path";
2
- import { bold, cyan, dim, gray, green, log, yellow } from "../lib/logger.js";
3
- import { maskToken } from "../lib/notion-env.js";
4
- import { notionApiError } from "../lib/notion-client.js";
5
- import { createNotionClient, fetchSchemas, loadSyncContext, } from "../lib/notion-sync.js";
6
- import { executePullEntry, planPull, } from "../lib/task-pull.js";
7
- function emptyReport(cwd, dryRun) {
8
- return {
9
- cwd,
10
- ok: false,
11
- dryRun,
12
- tokenMasked: null,
13
- notion: null,
14
- filter: { projectId: "", statusNames: [], limit: 0 },
15
- considered: 0,
16
- entries: [],
17
- skipped: [],
18
- trace: [],
19
- errors: [],
20
- };
21
- }
22
- function parseStatusList(raw) {
23
- if (typeof raw !== "string" || raw.trim().length === 0)
24
- return ["Planned"];
25
- return raw
26
- .split(",")
27
- .map((s) => s.trim())
28
- .filter((s) => s.length > 0);
29
- }
30
- function parseLimit(raw) {
31
- if (typeof raw === "number")
32
- return raw;
33
- if (typeof raw === "string" && raw.length > 0) {
34
- const n = Number.parseInt(raw, 10);
35
- if (Number.isFinite(n) && n > 0)
36
- return n;
37
- }
38
- return 20;
39
- }
40
- export async function taskPullCommand(options = {}) {
41
- const cwd = resolve(options.cwd ?? process.cwd());
42
- const dryRun = options.dryRun === true;
43
- const wantJson = options.json === true;
44
- const verbose = options.verbose === true;
45
- const report = emptyReport(cwd, dryRun);
46
- const ctxRes = await loadSyncContext(cwd);
47
- if (!ctxRes.ok) {
48
- report.errors.push({ reason: ctxRes.reason, message: ctxRes.message });
49
- return finalize(report, wantJson, verbose);
50
- }
51
- const ctx = ctxRes;
52
- report.tokenMasked = maskToken(ctx.token);
53
- report.notion = {
54
- projectsTargetId: ctx.notion.projectsTargetId,
55
- tasksTargetId: ctx.notion.tasksTargetId,
56
- projectsDatabaseId: ctx.notion.projectsDatabaseId,
57
- tasksDatabaseId: ctx.notion.tasksDatabaseId,
58
- };
59
- const statusNames = parseStatusList(options.status);
60
- const limit = parseLimit(options.limit);
61
- report.filter = {
62
- projectId: ctx.project.projectId,
63
- statusNames: [...statusNames],
64
- limit,
65
- };
66
- let client;
67
- try {
68
- client = await createNotionClient(ctx.token);
69
- }
70
- catch (err) {
71
- const apiErr = notionApiError(err);
72
- report.errors.push({
73
- reason: "sdk-load",
74
- message: `Failed to load @notionhq/client — ${apiErr.message}`,
75
- details: apiErr,
76
- });
77
- return finalize(report, wantJson, verbose);
78
- }
79
- const schemaRes = await fetchSchemas(client, ctx.notion);
80
- if (!schemaRes.ok) {
81
- report.errors.push({
82
- reason: schemaRes.reason,
83
- message: explainNotionError(schemaRes.error),
84
- details: schemaRes.error,
85
- });
86
- return finalize(report, wantJson, verbose);
87
- }
88
- const violations = [
89
- ...schemaRes.projects.violations,
90
- ...schemaRes.tasks.violations,
91
- ];
92
- if (violations.length > 0) {
93
- report.errors.push({
94
- reason: "schema",
95
- message: `Notion DB schema does not match VibeOps requirements (${violations.length} violation${violations.length === 1 ? "" : "s"}). Inspect details with \`vibeops notion test\`.`,
96
- details: violations,
97
- });
98
- return finalize(report, wantJson, verbose);
99
- }
100
- let plan;
101
- try {
102
- plan = await planPull({
103
- cwd,
104
- client,
105
- tasksDataSourceId: schemaRes.tasks.resolvedId,
106
- projectId: ctx.project.projectId,
107
- statusNames,
108
- limit,
109
- });
110
- }
111
- catch (err) {
112
- const apiErr = notionApiError(err);
113
- report.errors.push({
114
- reason: "query",
115
- message: explainNotionError(apiErr),
116
- details: apiErr,
117
- });
118
- return finalize(report, wantJson, verbose);
119
- }
120
- report.considered = plan.considered;
121
- for (const e of plan.entries) {
122
- report.entries.push({
123
- taskId: e.taskId,
124
- title: e.title,
125
- status: e.status,
126
- mvpPhase: e.mvpPhase,
127
- pageId: e.pageId,
128
- docsRelativePath: e.docsRelativePath,
129
- notionNeedsDocsPath: e.notionNeedsDocsPath,
130
- ...(e.detail !== undefined ? { detail: e.detail } : {}),
131
- });
132
- }
133
- for (const s of plan.skipped) {
134
- report.skipped.push({
135
- pageId: s.pageId,
136
- taskId: s.taskId,
137
- reason: s.reason,
138
- docsRelativePath: s.docsRelativePath,
139
- ...(s.detail !== undefined ? { detail: s.detail } : {}),
140
- });
141
- }
142
- for (const t of plan.trace) {
143
- report.trace.push({
144
- taskId: t.taskId,
145
- pageId: t.pageId,
146
- notionDocsPath: t.notionDocsPath,
147
- localResolvedPath: t.localResolvedPath,
148
- decision: t.decision,
149
- reason: t.reason,
150
- });
151
- }
152
- if (dryRun) {
153
- report.ok = true;
154
- return finalize(report, wantJson, verbose);
155
- }
156
- let mutateFailed = false;
157
- for (let i = 0; i < plan.entries.length; i++) {
158
- const entry = plan.entries[i];
159
- try {
160
- const res = await executePullEntry(client, entry);
161
- report.entries[i].created = true;
162
- report.entries[i].notionUpdated = res.notionUpdated;
163
- }
164
- catch (err) {
165
- mutateFailed = true;
166
- const apiErr = notionApiError(err);
167
- report.errors.push({
168
- reason: "pull-execute",
169
- message: `${entry.taskId}: ${explainNotionError(apiErr)}`,
170
- details: apiErr,
171
- });
172
- }
173
- }
174
- report.ok = !mutateFailed;
175
- finalize(report, wantJson, verbose);
176
- }
177
- function explainNotionError(err) {
178
- const tail = err.status ? ` (HTTP ${err.status})` : "";
179
- switch (err.code) {
180
- case "unauthorized":
181
- return `NOTION_TOKEN was rejected. Verify the integration is not expired and the value is correct.${tail}`;
182
- case "restricted_resource":
183
- return `The Notion DB is not shared with the integration. Add it via Notion DB → Connections.${tail}`;
184
- case "object_not_found":
185
- return `Notion resource not found. Verify the database id / page id.${tail}`;
186
- case "validation_error":
187
- return `Request rejected (validation_error): ${err.message}${tail}`;
188
- case "rate_limited":
189
- return `Notion API rate limit — retry shortly.${tail}`;
190
- case "request_timeout":
191
- case "ETIMEDOUT":
192
- return `Notion API 5s timeout. Check your network.${tail}`;
193
- default:
194
- return `${err.code}: ${err.message}${tail}`;
195
- }
196
- }
197
- function finalize(report, wantJson, verbose) {
198
- if (wantJson) {
199
- process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
200
- process.exitCode = report.ok ? 0 : 1;
201
- return;
202
- }
203
- log.info(bold("vibeops task pull"));
204
- log.info(` ${dim("cwd")} ${report.cwd}${report.dryRun ? ` ${yellow("[dry-run]")}` : ""}`);
205
- if (report.tokenMasked)
206
- log.info(` ${dim("token")} ${report.tokenMasked}`);
207
- if (report.filter.projectId.length > 0) {
208
- log.info(` ${dim("filter")} Project ID=${cyan(report.filter.projectId)} Status ∈ {${report.filter.statusNames
209
- .map((s) => cyan(s))
210
- .join(", ")}} ${dim(`limit=${report.filter.limit}`)}`);
211
- }
212
- log.blank();
213
- for (const e of report.errors) {
214
- log.error(`${cyan(e.reason)} — ${e.message}`);
215
- }
216
- if (report.errors.length > 0)
217
- log.blank();
218
- log.info(` ${bold("considered")} ${report.considered} rows ${dim("→")} ${green(`new ${report.entries.length}`)} ${yellow(`skipped ${report.skipped.length}`)}`);
219
- log.blank();
220
- if (report.entries.length > 0) {
221
- log.info(bold("would create"));
222
- for (const e of report.entries) {
223
- const tag = e.created ? green("✓") : yellow("·");
224
- const sync = e.notionUpdated ? cyan(" notion.docsPath←") : "";
225
- log.info(` ${tag} ${cyan(e.taskId)} ${e.title}${gray(` status=${e.status} phase=${e.mvpPhase}`)}${sync}`);
226
- log.info(` ${dim(e.docsRelativePath)}`);
227
- if (verbose && typeof e.detail === "string" && e.detail.length > 0) {
228
- for (const line of e.detail.split(/\r?\n/)) {
229
- log.info(` ${dim(line)}`);
230
- }
231
- }
232
- }
233
- log.blank();
234
- }
235
- if (report.skipped.length > 0) {
236
- log.info(bold("skipped"));
237
- for (const s of report.skipped) {
238
- log.info(` ${yellow("·")} ${cyan(s.taskId)} ${gray(s.reason)} ${dim(s.docsRelativePath)}`);
239
- if (typeof s.detail === "string" && s.detail.length > 0) {
240
- // Print the per-skip detail unconditionally — the new mismatch /
241
- // duplicate-task-id branches NEED to call out the Notion docs path
242
- // and the action the user should take.
243
- for (const line of s.detail.split(/\r?\n/)) {
244
- log.info(` ${dim(line)}`);
245
- }
246
- }
247
- }
248
- log.blank();
249
- }
250
- if (verbose && report.trace.length > 0) {
251
- log.info(bold("trace"));
252
- for (const t of report.trace) {
253
- log.info(` ${cyan(t.taskId)} ${gray(t.decision)} ${dim(`page=${t.pageId}`)}`);
254
- log.info(` ${dim(`notion docs path : ${t.notionDocsPath || "(empty)"}`)}`);
255
- log.info(` ${dim(`local resolved : ${t.localResolvedPath || "(none)"}`)}`);
256
- log.info(` ${dim(`reason : ${t.reason}`)}`);
257
- }
258
- log.blank();
259
- }
260
- if (report.dryRun) {
261
- log.info(yellow(" dry-run — no file or Notion mutation performed."));
262
- }
263
- if (report.ok && report.errors.length === 0) {
264
- log.ok(report.dryRun
265
- ? "Pull plan OK — re-run without --dry-run to apply."
266
- : `task pull complete — ${report.entries.filter((e) => e.created).length} file(s) created.`);
267
- process.exitCode = 0;
268
- }
269
- else if (report.ok) {
270
- log.warn("Some rows were skipped.");
271
- process.exitCode = 0;
272
- }
273
- else {
274
- log.error("task pull failed — see errors above.");
275
- process.exitCode = 1;
276
- }
277
- }
@@ -1,174 +0,0 @@
1
- import { relative, resolve } from "node:path";
2
- import { gitBranchExists, gitCheckout, gitDeleteBranch, gitResetHard, readGitInfo, } from "../lib/git.js";
3
- import { bold, cyan, dim, log, red, yellow } from "../lib/logger.js";
4
- import { projectPaths } from "../lib/paths.js";
5
- import { findTaskFile, readGitContext, readTaskFile, statusDisplay } from "../lib/task.js";
6
- function relOrAbs(root, p) {
7
- const r = relative(root, p);
8
- return r === "" ? "." : r.startsWith("..") ? p : r;
9
- }
10
- function shellQuote(s) {
11
- return /^[A-Za-z0-9._/\-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
12
- }
13
- function strategyCommands(strategy, ctx) {
14
- switch (strategy) {
15
- case "branch-delete":
16
- return [
17
- `git switch ${shellQuote(ctx.baseBranch)}`,
18
- `git branch -D ${shellQuote(ctx.taskBranch)}`,
19
- ];
20
- case "reset-base":
21
- return [
22
- `git switch ${shellQuote(ctx.taskBranch)}`,
23
- `git reset --hard ${shellQuote(ctx.baseCommit)}`,
24
- ];
25
- case "revert-merge":
26
- return [
27
- `git switch ${shellQuote(ctx.baseBranch)}`,
28
- `# find the merge commit:`,
29
- `git log --oneline --merges ${shellQuote(ctx.baseBranch)} | head -5`,
30
- `# then:`,
31
- `git revert -m 1 <merge-sha>`,
32
- ];
33
- }
34
- }
35
- function strategyRisk(strategy) {
36
- switch (strategy) {
37
- case "branch-delete":
38
- return "The task branch will be deleted along with any unmerged changes on it.";
39
- case "reset-base":
40
- return "Hard-reset the current branch to the base commit. Uncommitted and unpushed changes will be lost.";
41
- case "revert-merge":
42
- return "For already-merged-and-pushed work: adds new revert commits while keeping history. VibeOps never force-pushes.";
43
- }
44
- }
45
- const STRATEGY_LIST = ["branch-delete", "reset-base", "revert-merge"];
46
- function isDestructive(strategy) {
47
- return strategy === "reset-base";
48
- }
49
- export async function taskRollbackCommand(taskId, options = {}) {
50
- const cwd = resolve(options.cwd ?? process.cwd());
51
- const paths = projectPaths(cwd);
52
- const taskFile = await findTaskFile(paths.docsTasks, taskId);
53
- if (!taskFile) {
54
- log.error(`TASK not found: ${taskId} (looked in ${relOrAbs(cwd, paths.docsTasks)})`);
55
- process.exitCode = 1;
56
- return;
57
- }
58
- const meta = await readTaskFile(taskFile);
59
- const ctx = await readGitContext(taskFile);
60
- const git = await readGitInfo(cwd);
61
- log.info(bold(`vibeops task rollback ${meta.id}`));
62
- log.info(` ${dim("file")} ${relOrAbs(cwd, taskFile)}`);
63
- log.info(` ${dim("status")} ${statusDisplay(meta.status)}`);
64
- if (git.isRepo) {
65
- log.info(` ${dim("branch")} ${git.branch ?? dim("(detached)")}`);
66
- log.info(` ${dim("dirty")} ${git.dirty ? yellow("yes") : "no"}`);
67
- }
68
- else {
69
- log.info(` ${red("✗")} not a git repository`);
70
- }
71
- log.blank();
72
- if (ctx === null) {
73
- log.error(`Git Context not recorded in ${relOrAbs(cwd, taskFile)}. Was \`vibeops task start ${meta.id}\` ever run?`);
74
- log.info(`If you started the branch manually, add a "## Git Context" section to the TASK file with: Base Branch, Base Commit, Task Branch, Started At.`);
75
- process.exitCode = 1;
76
- return;
77
- }
78
- log.info(bold("Git Context (from TASK file)"));
79
- log.info(` ${dim("base branch")} ${ctx.baseBranch}`);
80
- log.info(` ${dim("base commit")} ${ctx.baseCommit}`);
81
- log.info(` ${dim("task branch")} ${cyan(ctx.taskBranch)}`);
82
- log.info(` ${dim("started at")} ${ctx.startedAt}`);
83
- log.blank();
84
- log.info(bold("Available strategies"));
85
- for (const s of STRATEGY_LIST) {
86
- const destructive = isDestructive(s);
87
- const tag = destructive ? red("destructive") : yellow("non-destructive");
88
- log.info(` ${bold(s)} ${dim("·")} ${tag}`);
89
- log.info(` ${dim(strategyRisk(s))}`);
90
- for (const cmd of strategyCommands(s, ctx)) {
91
- if (cmd.startsWith("#")) {
92
- log.info(` ${dim(cmd)}`);
93
- }
94
- else {
95
- log.info(` ${dim("$")} ${cmd}`);
96
- }
97
- }
98
- log.blank();
99
- }
100
- const wantConfirm = options.confirm === true || options.confirmDestructive === true;
101
- if (!wantConfirm) {
102
- log.info(`${yellow("!")} guidance only. add ${cyan("--confirm")} (non-destructive) or ${cyan("--confirm-destructive")} (allows hard reset) to actually run.`);
103
- return;
104
- }
105
- const strategy = options.strategy ?? "branch-delete";
106
- if (!STRATEGY_LIST.includes(strategy)) {
107
- log.error(`Unknown strategy: ${strategy}. Choose from: ${STRATEGY_LIST.join(", ")}.`);
108
- process.exitCode = 1;
109
- return;
110
- }
111
- if (strategy === "revert-merge") {
112
- log.info(`${yellow("!")} ${bold("revert-merge")} is never executed automatically. Run the commands above manually. ` +
113
- `(VibeOps never force-pushes, regardless of flag combination.)`);
114
- return;
115
- }
116
- if (isDestructive(strategy) && options.confirmDestructive !== true) {
117
- log.error(`${strategy} is a destructive operation. --confirm alone is not enough; pass --confirm-destructive to proceed.`);
118
- process.exitCode = 1;
119
- return;
120
- }
121
- if (!git.isRepo) {
122
- log.error("Cannot run rollback: not a git repository.");
123
- process.exitCode = 1;
124
- return;
125
- }
126
- if (git.dirty === true && options.confirmDestructive !== true) {
127
- log.error("Working tree is dirty. Commit / stash first, or rerun with --confirm-destructive to acknowledge the risk.");
128
- process.exitCode = 1;
129
- return;
130
- }
131
- if (options.dryRun === true) {
132
- log.info(bold(`dry-run — would run for strategy=${strategy}:`));
133
- for (const cmd of strategyCommands(strategy, ctx)) {
134
- if (cmd.startsWith("#"))
135
- continue;
136
- log.info(` ${dim("$")} ${cmd}`);
137
- }
138
- log.blank();
139
- log.info(dim("no git command was executed."));
140
- return;
141
- }
142
- if (strategy === "branch-delete") {
143
- if (git.branch === ctx.taskBranch) {
144
- log.info(`switching off ${ctx.taskBranch} → ${ctx.baseBranch}`);
145
- await gitCheckout(cwd, ctx.baseBranch);
146
- }
147
- if (!(await gitBranchExists(cwd, ctx.taskBranch))) {
148
- log.warn(`task branch already absent: ${ctx.taskBranch}`);
149
- }
150
- else {
151
- if (options.keepBranch === true) {
152
- log.info(`--keep-branch given → leaving ${ctx.taskBranch} intact.`);
153
- }
154
- else {
155
- await gitDeleteBranch(cwd, ctx.taskBranch, { force: true });
156
- log.ok(`deleted branch: ${ctx.taskBranch}`);
157
- }
158
- }
159
- log.blank();
160
- log.info(`Done. Current branch: ${cyan(ctx.baseBranch)}. TASK file Status is unchanged on purpose — edit it manually if needed.`);
161
- return;
162
- }
163
- if (strategy === "reset-base") {
164
- if (git.branch !== ctx.taskBranch) {
165
- log.info(`switching to ${ctx.taskBranch}`);
166
- await gitCheckout(cwd, ctx.taskBranch);
167
- }
168
- await gitResetHard(cwd, ctx.baseCommit);
169
- log.ok(`hard reset ${ctx.taskBranch} → ${ctx.baseCommit}`);
170
- log.blank();
171
- log.info(`Done. Branch ${cyan(ctx.taskBranch)} now points at ${ctx.baseCommit}. TASK file Status is unchanged on purpose.`);
172
- return;
173
- }
174
- }