vibe-coding-master 0.0.8 → 0.0.10

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 (30) hide show
  1. package/README.md +35 -24
  2. package/dist/backend/adapters/filesystem.js +0 -7
  3. package/dist/backend/api/app-settings-routes.js +8 -0
  4. package/dist/backend/api/message-routes.js +3 -1
  5. package/dist/backend/api/session-routes.js +7 -1
  6. package/dist/backend/api/task-routes.js +3 -10
  7. package/dist/backend/api/translation-routes.js +21 -3
  8. package/dist/backend/runtime/terminal-submit.js +20 -0
  9. package/dist/backend/server.js +8 -4
  10. package/dist/backend/services/app-settings-service.js +118 -15
  11. package/dist/backend/services/claude-transcript-service.js +12 -8
  12. package/dist/backend/services/command-dispatcher.js +2 -1
  13. package/dist/backend/services/message-service.js +10 -6
  14. package/dist/backend/services/project-service.js +5 -27
  15. package/dist/backend/services/session-service.js +7 -4
  16. package/dist/backend/services/task-service.js +66 -57
  17. package/dist/backend/services/translation-service.js +264 -77
  18. package/dist/backend/templates/harness/gitignore.js +1 -4
  19. package/dist/shared/types/app-settings.js +1 -0
  20. package/dist-frontend/assets/index-B1vIIwLq.js +88 -0
  21. package/dist-frontend/assets/index-DPyKuEOz.css +32 -0
  22. package/dist-frontend/index.html +2 -2
  23. package/docs/cc-best-practices.md +4 -4
  24. package/docs/product-design.md +71 -31
  25. package/docs/v1-architecture-design.md +90 -56
  26. package/docs/v1-implementation-plan.md +76 -62
  27. package/package.json +3 -1
  28. package/dist/backend/ws/translation-ws.js +0 -35
  29. package/dist-frontend/assets/index-CuiNNOzj.css +0 -32
  30. package/dist-frontend/assets/index-D59GuHCR.js +0 -58
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { ROLE_NAMES } from "../../shared/constants.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
+ import { submitTerminalInput } from "../runtime/terminal-submit.js";
6
7
  import { renderManualStagePrompt, renderMessageEnvelope } from "../templates/message-envelope.js";
7
8
  const PM_ROLE = "project-manager";
8
9
  const PM_TO_ROLE_TYPES = new Set(["task", "question", "review-request", "revise", "cancel"]);
@@ -12,7 +13,7 @@ export function createMessageService(deps) {
12
13
  const id = deps.id ?? (() => `msg_${randomUUID()}`);
13
14
  return {
14
15
  listMessages(input) {
15
- return readLatestMessages(deps.fs, getMessagesPath(input.repoRoot, input.stateRoot, input.taskSlug));
16
+ return readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
16
17
  },
17
18
  async sendMessage(input) {
18
19
  await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
@@ -56,7 +57,7 @@ export function createMessageService(deps) {
56
57
  deliveredAt: timestamp,
57
58
  failureReason: undefined
58
59
  };
59
- deps.runtime.write(session.id, `${renderMessageEnvelope(delivered)}\r`);
60
+ await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivered));
60
61
  await appendMessageSnapshot(deps.fs, input, delivered);
61
62
  return { message: delivered, delivered: true, requiresUserApproval: false };
62
63
  },
@@ -95,7 +96,7 @@ export function createMessageService(deps) {
95
96
  return rejected;
96
97
  },
97
98
  async getOrchestrationState(input) {
98
- const statePath = getOrchestrationStatePath(input.repoRoot, input.stateRoot, input.taskSlug);
99
+ const statePath = getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
99
100
  if (!(await deps.fs.pathExists(statePath))) {
100
101
  return {
101
102
  taskSlug: input.taskSlug,
@@ -114,7 +115,7 @@ export function createMessageService(deps) {
114
115
  paused: input.paused ?? current.paused,
115
116
  updatedAt: now()
116
117
  };
117
- await deps.fs.writeJsonAtomic(getOrchestrationStatePath(input.repoRoot, input.stateRoot, input.taskSlug), next);
118
+ await deps.fs.writeJsonAtomic(getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), next);
118
119
  return next;
119
120
  }
120
121
  };
@@ -183,7 +184,7 @@ ${artifactRefs}
183
184
  `;
184
185
  }
185
186
  async function getMessageOrThrow(fs, input) {
186
- const messages = await readLatestMessages(fs, getMessagesPath(input.repoRoot, input.stateRoot, input.taskSlug));
187
+ const messages = await readLatestMessages(fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
187
188
  const message = messages.find((candidate) => candidate.id === input.messageId);
188
189
  if (!message) {
189
190
  throw new VcmError({
@@ -207,7 +208,7 @@ async function readLatestMessages(fs, messagesPath) {
207
208
  return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
208
209
  }
209
210
  async function appendMessageSnapshot(fs, input, message) {
210
- await fs.appendText(getMessagesPath(input.repoRoot, input.stateRoot, input.taskSlug), `${JSON.stringify(message)}\n`);
211
+ await fs.appendText(getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), `${JSON.stringify(message)}\n`);
211
212
  }
212
213
  function getMessagesPath(repoRoot, stateRoot, taskSlug) {
213
214
  return path.join(repoRoot, stateRoot, "messages", `${taskSlug}.jsonl`);
@@ -215,3 +216,6 @@ function getMessagesPath(repoRoot, stateRoot, taskSlug) {
215
216
  function getOrchestrationStatePath(repoRoot, stateRoot, taskSlug) {
216
217
  return path.join(repoRoot, stateRoot, "orchestration", `${taskSlug}.json`);
217
218
  }
219
+ function getStateRepoRoot(input) {
220
+ return input.stateRepoRoot ?? input.repoRoot;
221
+ }
@@ -3,20 +3,8 @@ import { ROLE_NAMES } from "../../shared/constants.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  const DEFAULT_HANDOFF_ROOT = ".ai/handoffs";
5
5
  const DEFAULT_STATE_ROOT = ".ai/vcm";
6
- const LEGACY_STATE_ROOT = ".vcm";
7
6
  export function createProjectService(deps) {
8
7
  let currentProject = null;
9
- async function migrateLegacyStateRoot(repoRoot) {
10
- if (!deps.fs.copyDir) {
11
- return;
12
- }
13
- const legacyStateRoot = path.join(repoRoot, LEGACY_STATE_ROOT);
14
- const currentStateRoot = path.join(repoRoot, DEFAULT_STATE_ROOT);
15
- if (!(await deps.fs.pathExists(legacyStateRoot)) || await deps.fs.pathExists(currentStateRoot)) {
16
- return;
17
- }
18
- await deps.fs.copyDir(legacyStateRoot, currentStateRoot);
19
- }
20
8
  return {
21
9
  async connectProject(input) {
22
10
  const requestedPath = input.repoPath.trim();
@@ -43,9 +31,6 @@ export function createProjectService(deps) {
43
31
  const config = await this.loadConfig(repoRoot);
44
32
  await deps.fs.ensureDir(path.join(repoRoot, config.handoffRoot));
45
33
  await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "tasks"));
46
- await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "sessions"));
47
- await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "messages"));
48
- await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "orchestration"));
49
34
  await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "worktrees"));
50
35
  await this.saveConfig(config, true);
51
36
  const warnings = [];
@@ -86,16 +71,9 @@ export function createProjectService(deps) {
86
71
  return deps.appSettings.getRecentRepositoryPaths();
87
72
  },
88
73
  async loadConfig(repoRoot) {
89
- const configPath = this.getConfigPath(repoRoot);
90
- if (await deps.fs.pathExists(configPath)) {
91
- return normalizeProjectConfig(await deps.fs.readJson(configPath), repoRoot);
92
- }
93
- const legacyConfigPath = path.join(repoRoot, LEGACY_STATE_ROOT, "config.json");
94
- if (await deps.fs.pathExists(legacyConfigPath)) {
95
- const migratedConfig = normalizeProjectConfig(await deps.fs.readJson(legacyConfigPath), repoRoot);
96
- await migrateLegacyStateRoot(repoRoot);
97
- await this.saveConfig(migratedConfig, true);
98
- return migratedConfig;
74
+ const appConfig = await deps.appSettings.loadProjectConfig(repoRoot);
75
+ if (appConfig) {
76
+ return normalizeProjectConfig(appConfig, repoRoot);
99
77
  }
100
78
  return buildDefaultProjectConfig(repoRoot);
101
79
  },
@@ -105,10 +83,10 @@ export function createProjectService(deps) {
105
83
  if (!force && await deps.fs.pathExists(configPath)) {
106
84
  return;
107
85
  }
108
- await deps.fs.writeJsonAtomic(configPath, normalizedConfig);
86
+ await deps.appSettings.saveProjectConfig(normalizedConfig);
109
87
  },
110
88
  getConfigPath(repoRoot) {
111
- return path.join(repoRoot, DEFAULT_STATE_ROOT, "config.json");
89
+ return deps.appSettings.getProjectConfigPath(repoRoot);
112
90
  }
113
91
  };
114
92
  }
@@ -16,7 +16,7 @@ export function createSessionService(deps) {
16
16
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
17
17
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
18
18
  const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
19
- const persisted = await loadPersistedRoleRecord(deps.fs, repoRoot, config.stateRoot, taskSlug, role);
19
+ const persisted = await loadPersistedRoleRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug, role);
20
20
  const permissionMode = input.permissionMode ?? persisted?.permissionMode ?? "default";
21
21
  const claudeSessionId = launchMode === "resume"
22
22
  ? persisted?.claudeSessionId
@@ -73,7 +73,7 @@ export function createSessionService(deps) {
73
73
  exitCode: runtimeSession.exitCode
74
74
  };
75
75
  deps.registry.upsert(record);
76
- await persistTaskSession(deps.fs, repoRoot, config.stateRoot, record);
76
+ await persistTaskSession(deps.fs, taskRepoRoot, config.stateRoot, record);
77
77
  await deps.taskService.updateTaskStatus(repoRoot, taskSlug, "running");
78
78
  return record;
79
79
  }
@@ -103,7 +103,8 @@ export function createSessionService(deps) {
103
103
  };
104
104
  deps.registry.upsert(updated);
105
105
  const config = await deps.projectService.loadConfig(repoRoot);
106
- await persistTaskSession(deps.fs, repoRoot, config.stateRoot, updated);
106
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
107
+ await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
107
108
  return updated;
108
109
  },
109
110
  async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
@@ -119,8 +120,10 @@ export function createSessionService(deps) {
119
120
  },
120
121
  async getRoleSession(repoRoot, taskSlug, role) {
121
122
  const config = await deps.projectService.loadConfig(repoRoot);
123
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
124
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
122
125
  const record = deps.registry.getByRole(taskSlug, role)
123
- ?? await loadPersistedRoleRecord(deps.fs, repoRoot, config.stateRoot, taskSlug, role);
126
+ ?? await loadPersistedRoleRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug, role);
124
127
  if (!record) {
125
128
  return undefined;
126
129
  }
@@ -8,8 +8,13 @@ export function createTaskService(deps) {
8
8
  assertValidTaskSlug(input.taskSlug);
9
9
  const config = await deps.projectService.loadConfig(repoRoot);
10
10
  const taskPath = getTaskPath(repoRoot, config.stateRoot, input.taskSlug);
11
- const branch = `feature/${input.taskSlug}`;
12
- const worktreePath = getTaskWorktreePath(repoRoot, config.stateRoot, input.taskSlug);
11
+ const shouldCreateWorktree = input.createWorktree !== false;
12
+ const taskBranch = shouldCreateWorktree
13
+ ? `feature/${input.taskSlug}`
14
+ : await deps.git.getCurrentBranch(repoRoot);
15
+ const worktreePath = shouldCreateWorktree
16
+ ? getTaskWorktreePath(repoRoot, config.stateRoot, input.taskSlug)
17
+ : undefined;
13
18
  if (await deps.fs.pathExists(taskPath)) {
14
19
  throw new VcmError({
15
20
  code: "TASK_EXISTS",
@@ -17,23 +22,7 @@ export function createTaskService(deps) {
17
22
  statusCode: 409
18
23
  });
19
24
  }
20
- if (await deps.git.branchExists(repoRoot, branch)) {
21
- throw new VcmError({
22
- code: "TASK_BRANCH_EXISTS",
23
- message: `Task branch already exists: ${branch}`,
24
- statusCode: 409,
25
- hint: "Choose a different task name or clean up the existing branch."
26
- });
27
- }
28
- if (await deps.fs.pathExists(worktreePath)) {
29
- throw new VcmError({
30
- code: "TASK_WORKTREE_EXISTS",
31
- message: `Task worktree already exists: ${worktreePath}`,
32
- statusCode: 409,
33
- hint: "Choose a different task name or clean up the existing worktree."
34
- });
35
- }
36
- if (!(await deps.git.isIgnored(repoRoot, `${config.stateRoot}/config.json`))) {
25
+ if (!(await deps.git.isIgnored(repoRoot, `${config.stateRoot}/tasks/.probe`))) {
37
26
  throw new VcmError({
38
27
  code: "VCM_STATE_NOT_IGNORED",
39
28
  message: `${config.stateRoot}/ is not ignored by Git.`,
@@ -41,23 +30,42 @@ export function createTaskService(deps) {
41
30
  hint: "Apply VCM Harness first so .gitignore contains the VCM managed block."
42
31
  });
43
32
  }
44
- const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
45
- if (baseStatus.trim()) {
46
- throw new VcmError({
47
- code: "BASE_REPO_DIRTY",
48
- message: "The connected repository has uncommitted changes.",
49
- statusCode: 409,
50
- hint: "Commit, stash, or discard base repository changes before creating a task worktree."
33
+ if (shouldCreateWorktree && worktreePath) {
34
+ if (await deps.git.branchExists(repoRoot, taskBranch)) {
35
+ throw new VcmError({
36
+ code: "TASK_BRANCH_EXISTS",
37
+ message: `Task branch already exists: ${taskBranch}`,
38
+ statusCode: 409,
39
+ hint: "Choose a different task name or clean up the existing branch."
40
+ });
41
+ }
42
+ if (await deps.fs.pathExists(worktreePath)) {
43
+ throw new VcmError({
44
+ code: "TASK_WORKTREE_EXISTS",
45
+ message: `Task worktree already exists: ${worktreePath}`,
46
+ statusCode: 409,
47
+ hint: "Choose a different task name or clean up the existing worktree."
48
+ });
49
+ }
50
+ const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
51
+ if (baseStatus.trim()) {
52
+ throw new VcmError({
53
+ code: "BASE_REPO_DIRTY",
54
+ message: "The connected repository has uncommitted changes.",
55
+ statusCode: 409,
56
+ hint: "Commit, stash, or discard base repository changes before creating a task worktree."
57
+ });
58
+ }
59
+ await deps.fs.ensureDir(path.dirname(worktreePath));
60
+ await deps.git.createWorktree({
61
+ repoRoot,
62
+ branch: taskBranch,
63
+ worktreePath,
64
+ baseRef: "HEAD"
51
65
  });
52
66
  }
53
- await deps.fs.ensureDir(path.dirname(worktreePath));
54
- await deps.git.createWorktree({
55
- repoRoot,
56
- branch,
57
- worktreePath,
58
- baseRef: "HEAD"
59
- });
60
67
  const timestamp = now();
68
+ const taskRepoRoot = worktreePath ?? repoRoot;
61
69
  const task = {
62
70
  version: 1,
63
71
  taskSlug: input.taskSlug,
@@ -66,19 +74,20 @@ export function createTaskService(deps) {
66
74
  updatedAt: timestamp,
67
75
  repoRoot,
68
76
  worktreePath,
69
- branch,
77
+ branch: taskBranch,
70
78
  handoffDir: path.posix.join(config.handoffRoot, input.taskSlug),
71
79
  status: "created",
72
80
  specPath: input.specPath,
73
81
  cleanupStatus: "active"
74
82
  };
83
+ await ensureTaskRuntimeStateDirs(deps.fs, taskRepoRoot, config.stateRoot);
75
84
  await deps.artifactService.ensureHandoffStructure({
76
- repoRoot: worktreePath,
85
+ repoRoot: taskRepoRoot,
77
86
  taskSlug: input.taskSlug,
78
87
  handoffDir: task.handoffDir
79
88
  });
80
89
  await deps.artifactService.createArtifactTemplates({
81
- repoRoot: worktreePath,
90
+ repoRoot: taskRepoRoot,
82
91
  taskSlug: input.taskSlug,
83
92
  handoffDir: task.handoffDir
84
93
  });
@@ -136,30 +145,23 @@ export function createTaskService(deps) {
136
145
  }
137
146
  const config = await deps.projectService.loadConfig(repoRoot);
138
147
  const task = await this.loadTask(repoRoot, taskSlug);
148
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
149
+ const statePaths = getTaskStatePaths(repoRoot, taskRepoRoot, config.stateRoot, taskSlug);
139
150
  const removedStatePaths = [];
140
151
  const cleanedAt = now();
141
152
  if (task.worktreePath) {
142
153
  assertTaskWorktreePath(repoRoot, config.stateRoot, task.worktreePath);
143
- const status = await deps.git.getStatusPorcelain(task.worktreePath);
144
- if (status.trim() && !options.force) {
145
- throw new VcmError({
146
- code: "TASK_WORKTREE_DIRTY",
147
- message: `Task worktree has uncommitted changes: ${task.worktreePath}`,
148
- statusCode: 409,
149
- hint: "Commit, stash, or discard the task worktree changes before cleanup, or retry with force."
150
- });
151
- }
152
- await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force });
153
- }
154
- for (const statePath of getTaskStatePaths(repoRoot, config.stateRoot, taskSlug)) {
155
- await deps.fs.removePath(statePath, { force: true });
156
- removedStatePaths.push(statePath);
154
+ await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
157
155
  }
158
156
  let deletedBranch;
159
- if (options.deleteBranch) {
160
- await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch });
157
+ if (task.worktreePath && (options.deleteBranch ?? true)) {
158
+ await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch ?? true });
161
159
  deletedBranch = task.branch;
162
160
  }
161
+ for (const statePath of statePaths) {
162
+ await deps.fs.removePath(statePath, { recursive: true, force: true });
163
+ removedStatePaths.push(statePath);
164
+ }
163
165
  return {
164
166
  taskSlug,
165
167
  removedWorktreePath: task.worktreePath,
@@ -179,12 +181,19 @@ function getTaskPath(repoRoot, stateRoot, taskSlug) {
179
181
  function getTaskWorktreePath(repoRoot, stateRoot, taskSlug) {
180
182
  return path.join(repoRoot, stateRoot, "worktrees", taskSlug);
181
183
  }
182
- function getTaskStatePaths(repoRoot, stateRoot, taskSlug) {
184
+ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
185
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "sessions"));
186
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "messages"));
187
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "orchestration"));
188
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
189
+ }
190
+ function getTaskStatePaths(baseRepoRoot, taskRepoRoot, stateRoot, taskSlug) {
183
191
  return [
184
- path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`),
185
- path.join(repoRoot, stateRoot, "sessions", `${taskSlug}.json`),
186
- path.join(repoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
187
- path.join(repoRoot, stateRoot, "orchestration", `${taskSlug}.json`)
192
+ path.join(baseRepoRoot, stateRoot, "tasks", `${taskSlug}.json`),
193
+ path.join(taskRepoRoot, stateRoot, "sessions", `${taskSlug}.json`),
194
+ path.join(taskRepoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
195
+ path.join(taskRepoRoot, stateRoot, "orchestration", `${taskSlug}.json`),
196
+ path.join(taskRepoRoot, stateRoot, "translation", taskSlug)
188
197
  ];
189
198
  }
190
199
  function assertTaskWorktreePath(repoRoot, stateRoot, worktreePath) {