vibe-coding-master 0.0.12 → 0.0.13
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/README.md +24 -9
- package/dist/backend/api/round-routes.js +33 -0
- package/dist/backend/api/session-routes.js +2 -0
- package/dist/backend/api/task-routes.js +1 -0
- package/dist/backend/server.js +19 -3
- package/dist/backend/services/app-settings-service.js +2 -1
- package/dist/backend/services/message-service.js +158 -55
- package/dist/backend/services/project-service.js +1 -1
- package/dist/backend/services/round-service.js +227 -0
- package/dist/backend/services/task-service.js +14 -6
- package/dist/backend/templates/harness/architect-agent.js +1 -0
- package/dist/backend/templates/harness/claude-root.js +3 -0
- package/dist/backend/templates/harness/coder-agent.js +1 -0
- package/dist/backend/templates/harness/gitignore.js +2 -1
- package/dist/backend/templates/harness/project-manager-agent.js +3 -0
- package/dist/backend/templates/harness/reviewer-agent.js +1 -0
- package/dist/backend/ws/terminal-ws.js +15 -1
- package/dist/shared/types/round.js +1 -0
- package/dist-frontend/assets/index-CyJrJge9.js +88 -0
- package/dist-frontend/assets/index-N5DA0uE9.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +7 -4
- package/docs/product-design.md +59 -25
- package/docs/v1-architecture-design.md +86 -27
- package/docs/v1-implementation-plan.md +74 -12
- package/package.json +1 -1
- package/dist-frontend/assets/index-Bi4X3GSR.css +0 -32
- package/dist-frontend/assets/index-DaHXq14j.js +0 -88
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ The `Create worktree and branch` option is selected by default when creating a t
|
|
|
170
170
|
|
|
171
171
|
- task name: `<task>`
|
|
172
172
|
- branch: `feature/<task>`
|
|
173
|
-
- worktree path: `.
|
|
173
|
+
- worktree path: `.claude/worktrees/<task>` inside the connected base repository
|
|
174
174
|
- role session cwd: that task worktree
|
|
175
175
|
|
|
176
176
|
VCM will not create worktrees per role. `project-manager`, `architect`, `coder`, and `reviewer` for the same task share the same task worktree.
|
|
@@ -179,7 +179,7 @@ The user can turn this option off. In that mode, VCM creates the task metadata a
|
|
|
179
179
|
|
|
180
180
|
VCM will not offer a separate `Create task worktree` button after a task exists, and a task should not be switched to another branch/worktree mode after creation.
|
|
181
181
|
|
|
182
|
-
Because worktrees live under `.
|
|
182
|
+
Because worktrees live under `.claude/worktrees/`, the connected repository must ignore both `.ai/vcm/` and `.claude/worktrees/`. Apply the VCM Harness before creating tasks so `.gitignore` contains the managed ignore block. The base repository must also be clean because the task branch/worktree is created from the connected repo's current `HEAD`.
|
|
183
183
|
|
|
184
184
|
When a task is complete, VCM provides a red `Close Task` action. Closing a task shows a destructive confirmation, stops VCM-managed running role sessions for that task, then deletes the task worktree, deletes the task branch by default, removes the base task index entry, and removes task runtime metadata. VCM does not preflight running sessions or uncommitted changes before closing. Tasks created without a worktree only remove VCM metadata because they do not own a separate branch/worktree.
|
|
185
185
|
|
|
@@ -190,7 +190,7 @@ The left sidebar is intentionally compact and collapsible:
|
|
|
190
190
|
- `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
|
|
191
191
|
- `Repository`: connected path, branch, and working tree state. `Working tree: uncommitted changes` means `git status --porcelain` is not empty.
|
|
192
192
|
- `Workflow`: current soft gate and five workflow steps.
|
|
193
|
-
- `Settings`: `Theme`, `
|
|
193
|
+
- `Settings`: `Theme`, `Round alert`, `Messages`, and `Events`.
|
|
194
194
|
- `VCM Harness`: status for `CLAUDE.md`, role agent files, and `.gitignore`.
|
|
195
195
|
- `New Task`: one `task name` input.
|
|
196
196
|
- `Tasks`: task list and task status.
|
|
@@ -199,7 +199,7 @@ All sidebar sections are collapsed by default. When no task is selected, `Reposi
|
|
|
199
199
|
|
|
200
200
|
## Translation
|
|
201
201
|
|
|
202
|
-
The `
|
|
202
|
+
The role toolbar has an `Auto orchestration` on/off button immediately to the left of the `Translate` button. The `Translate` button opens a translation panel beside the embedded terminal. The terminal and translation panel split the available width evenly.
|
|
203
203
|
|
|
204
204
|
Translation settings are local and stored in:
|
|
205
205
|
|
|
@@ -211,6 +211,8 @@ The same file stores recent repository paths. The translation API key is stored
|
|
|
211
211
|
|
|
212
212
|
The sidebar `Settings` section also stores the UI theme preference in this file. The default is `system`, which follows the OS/browser color-scheme preference; users can cycle between `System`, `Light`, and `Dark`.
|
|
213
213
|
|
|
214
|
+
The same sidebar also has a `Round alert` toggle. It is on by default and controls the in-app prompt plus short sound that fires when VCM detects that the current full conversation round is complete.
|
|
215
|
+
|
|
214
216
|
Translation behavior:
|
|
215
217
|
|
|
216
218
|
- Provider type is OpenAI-compatible chat completions.
|
|
@@ -255,10 +257,11 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
|
|
|
255
257
|
```gitignore
|
|
256
258
|
# VCM:BEGIN version=1
|
|
257
259
|
.ai/vcm/
|
|
260
|
+
.claude/worktrees/
|
|
258
261
|
# VCM:END
|
|
259
262
|
```
|
|
260
263
|
|
|
261
|
-
`.ai/vcm/` is the active VCM local control area. The base repo keeps the task index
|
|
264
|
+
`.ai/vcm/` is the active VCM local control area, and `.claude/worktrees/` is the Claude-compatible task worktree area. The base repo keeps the task index; each task runtime repo keeps its own session, message, orchestration, and translation state.
|
|
262
265
|
|
|
263
266
|
After applying harness changes, VCM reports the exact files changed and reminds the user to review and commit them before starting long-running work.
|
|
264
267
|
|
|
@@ -301,7 +304,7 @@ The backend also keeps a compatibility role-command dispatch endpoint, but the p
|
|
|
301
304
|
|
|
302
305
|
## Orchestration Modes
|
|
303
306
|
|
|
304
|
-
VCM has a task-level `Auto orchestration` switch in the
|
|
307
|
+
VCM has a task-level `Auto orchestration` switch in the active role toolbar, immediately to the left of `Translate`.
|
|
305
308
|
|
|
306
309
|
When it is off, VCM is in manual mode:
|
|
307
310
|
|
|
@@ -317,12 +320,24 @@ When it is on, VCM is in auto mode:
|
|
|
317
320
|
- Backend policy still applies.
|
|
318
321
|
- PM can send work to `architect`, `coder`, or `reviewer`.
|
|
319
322
|
- Non-PM roles can reply only to `project-manager`.
|
|
320
|
-
- If the target role session is running, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it.
|
|
323
|
+
- If the target role session is running and has no active delivered message, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it.
|
|
324
|
+
- When the GUI observes a newly delivered auto message, it switches the active role tab to that message's target role so the user can watch the next step.
|
|
325
|
+
- VCM enforces per-role turn-taking: each target role can have at most one in-flight delivered message.
|
|
326
|
+
- Additional messages to a busy target role are kept as `queued` and are not written to that terminal.
|
|
327
|
+
- When a role sends `vcmctl reply` or `vcmctl result`, VCM acknowledges that role's active message and delivers the next queued message for that role.
|
|
321
328
|
|
|
322
329
|
The backend state model still contains a `paused` field for compatibility with existing API routes, but the current GUI exposes only a single on/off orchestration toggle.
|
|
323
330
|
|
|
324
331
|
The backend still exposes stage/approve/reject compatibility APIs for automation and future UI work. They are not primary controls in the current Messages modal.
|
|
325
332
|
|
|
333
|
+
## Round Completion Alerts
|
|
334
|
+
|
|
335
|
+
VCM detects conversation completion from Claude Code transcript JSONL events, not PTY silence. The backend watches each running role session and treats `stop_reason === "end_turn"` as a candidate end only after pending tool uses have finished and a short debounce passes.
|
|
336
|
+
|
|
337
|
+
For role chains, VCM waits for the latest delivered message's target role. For example, if PM sends work to Coder and Coder sends a result back to PM, the round is not complete when Coder finishes; it is complete only after PM finishes the final response. If no VCM role message is involved, the latest direct role answer can complete the round.
|
|
338
|
+
|
|
339
|
+
When `Round alert` is enabled, the frontend polls the task round state, deduplicates each completion id, shows a small `Round complete` prompt, and plays a short local sound.
|
|
340
|
+
|
|
326
341
|
## Resume Behavior
|
|
327
342
|
|
|
328
343
|
Each role session stores its Claude session id and transcript path under:
|
|
@@ -345,7 +360,7 @@ For a connected repository, VCM uses:
|
|
|
345
360
|
```text
|
|
346
361
|
~/.vcm/projects/<project-id>/config.json
|
|
347
362
|
<baseRepoRoot>/.ai/vcm/tasks/<task>.json
|
|
348
|
-
<baseRepoRoot>/.
|
|
363
|
+
<baseRepoRoot>/.claude/worktrees/<task>/
|
|
349
364
|
<taskRepoRoot>/.ai/vcm/sessions/<task>.json
|
|
350
365
|
<taskRepoRoot>/.ai/vcm/messages/<task>.jsonl
|
|
351
366
|
<taskRepoRoot>/.ai/vcm/orchestration/<task>.json
|
|
@@ -359,7 +374,7 @@ For a connected repository, VCM uses:
|
|
|
359
374
|
<taskRepoRoot>/.ai/vcm/handoffs/logs/{project-manager,architect,coder,reviewer}.log
|
|
360
375
|
```
|
|
361
376
|
|
|
362
|
-
The project config is stored under `~/.vcm` so it is durable local app state and is not hidden inside a Git-ignored repository directory. For worktree-backed tasks, `taskRepoRoot` is `<baseRepoRoot>/.
|
|
377
|
+
The project config is stored under `~/.vcm` so it is durable local app state and is not hidden inside a Git-ignored repository directory. For worktree-backed tasks, `taskRepoRoot` is `<baseRepoRoot>/.claude/worktrees/<task>`; for inline tasks, `taskRepoRoot` is the connected base repo.
|
|
363
378
|
|
|
364
379
|
Because handoffs are scoped to `taskRepoRoot` without an extra task-name directory, VCM allows only one active inline task per connected repository. Use the default worktree mode for parallel tasks.
|
|
365
380
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { VcmError } from "../errors.js";
|
|
2
|
+
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
3
|
+
export function registerRoundRoutes(app, deps) {
|
|
4
|
+
app.get("/api/tasks/:taskSlug/round", async (request) => {
|
|
5
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
6
|
+
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
7
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
8
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
9
|
+
const sessions = await deps.sessionService.listRoleSessions(project.repoRoot, request.params.taskSlug);
|
|
10
|
+
const messages = await deps.messageService.listMessages({
|
|
11
|
+
repoRoot: project.repoRoot,
|
|
12
|
+
stateRepoRoot: taskRepoRoot,
|
|
13
|
+
stateRoot: config.stateRoot,
|
|
14
|
+
taskSlug: request.params.taskSlug
|
|
15
|
+
});
|
|
16
|
+
return deps.roundService.getTaskRoundState({
|
|
17
|
+
taskSlug: request.params.taskSlug,
|
|
18
|
+
sessions,
|
|
19
|
+
messages
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function requireCurrentProject(projectService) {
|
|
24
|
+
const project = await projectService.getCurrentProject();
|
|
25
|
+
if (!project) {
|
|
26
|
+
throw new VcmError({
|
|
27
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
28
|
+
message: "Connect a repository first.",
|
|
29
|
+
statusCode: 409
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return project;
|
|
33
|
+
}
|
|
@@ -15,6 +15,7 @@ export function registerSessionRoutes(app, deps) {
|
|
|
15
15
|
const role = parseRole(request.params.role);
|
|
16
16
|
const session = await deps.sessionService.stopRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
17
17
|
await deps.translationService.stopSession(session.id);
|
|
18
|
+
deps.roundService.stopSession(session.id);
|
|
18
19
|
return session;
|
|
19
20
|
});
|
|
20
21
|
app.post("/api/tasks/:taskSlug/sessions/:role/restart", async (request) => {
|
|
@@ -23,6 +24,7 @@ export function registerSessionRoutes(app, deps) {
|
|
|
23
24
|
const existing = await deps.sessionService.getRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
24
25
|
if (existing) {
|
|
25
26
|
await deps.translationService.stopSession(existing.id, { clearCache: true });
|
|
27
|
+
deps.roundService.stopSession(existing.id);
|
|
26
28
|
}
|
|
27
29
|
return deps.sessionService.restartRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
28
30
|
});
|
|
@@ -22,6 +22,7 @@ export function registerTaskRoutes(app, deps) {
|
|
|
22
22
|
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
23
23
|
await stopRunningRoleSessions(deps, project.repoRoot, request.params.taskSlug);
|
|
24
24
|
await deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), request.params.taskSlug, { clearCache: true });
|
|
25
|
+
deps.roundService.stopTask(request.params.taskSlug);
|
|
25
26
|
return deps.taskService.cleanupTask(project.repoRoot, request.params.taskSlug, request.body ?? {});
|
|
26
27
|
});
|
|
27
28
|
}
|
package/dist/backend/server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createProjectService } from "./services/project-service.js";
|
|
|
18
18
|
import { createSessionRegistry } from "./runtime/session-registry.js";
|
|
19
19
|
import { createSessionService } from "./services/session-service.js";
|
|
20
20
|
import { createMessageService } from "./services/message-service.js";
|
|
21
|
+
import { createRoundService } from "./services/round-service.js";
|
|
21
22
|
import { createStatusService } from "./services/status-service.js";
|
|
22
23
|
import { createTaskService } from "./services/task-service.js";
|
|
23
24
|
import { createTranslationService } from "./services/translation-service.js";
|
|
@@ -26,6 +27,7 @@ import { registerArtifactRoutes } from "./api/artifact-routes.js";
|
|
|
26
27
|
import { registerHarnessRoutes } from "./api/harness-routes.js";
|
|
27
28
|
import { registerMessageRoutes } from "./api/message-routes.js";
|
|
28
29
|
import { registerProjectRoutes } from "./api/project-routes.js";
|
|
30
|
+
import { registerRoundRoutes } from "./api/round-routes.js";
|
|
29
31
|
import { registerSessionRoutes } from "./api/session-routes.js";
|
|
30
32
|
import { registerTaskRoutes } from "./api/task-routes.js";
|
|
31
33
|
import { registerTranslationRoutes } from "./api/translation-routes.js";
|
|
@@ -56,13 +58,15 @@ export async function createServer(deps, options = {}) {
|
|
|
56
58
|
taskService: deps.taskService,
|
|
57
59
|
sessionService: deps.sessionService,
|
|
58
60
|
statusService: deps.statusService,
|
|
59
|
-
translationService: deps.translationService
|
|
61
|
+
translationService: deps.translationService,
|
|
62
|
+
roundService: deps.roundService
|
|
60
63
|
});
|
|
61
64
|
registerSessionRoutes(app, {
|
|
62
65
|
projectService: deps.projectService,
|
|
63
66
|
sessionService: deps.sessionService,
|
|
64
67
|
commandDispatcher: deps.commandDispatcher,
|
|
65
|
-
translationService: deps.translationService
|
|
68
|
+
translationService: deps.translationService,
|
|
69
|
+
roundService: deps.roundService
|
|
66
70
|
});
|
|
67
71
|
registerArtifactRoutes(app, {
|
|
68
72
|
projectService: deps.projectService,
|
|
@@ -74,6 +78,13 @@ export async function createServer(deps, options = {}) {
|
|
|
74
78
|
taskService: deps.taskService,
|
|
75
79
|
messageService: deps.messageService
|
|
76
80
|
});
|
|
81
|
+
registerRoundRoutes(app, {
|
|
82
|
+
projectService: deps.projectService,
|
|
83
|
+
taskService: deps.taskService,
|
|
84
|
+
sessionService: deps.sessionService,
|
|
85
|
+
messageService: deps.messageService,
|
|
86
|
+
roundService: deps.roundService
|
|
87
|
+
});
|
|
77
88
|
registerTranslationRoutes(app, {
|
|
78
89
|
projectService: deps.projectService,
|
|
79
90
|
taskService: deps.taskService,
|
|
@@ -146,10 +157,14 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
146
157
|
sessionService,
|
|
147
158
|
taskService
|
|
148
159
|
});
|
|
160
|
+
const transcripts = createClaudeTranscriptService();
|
|
161
|
+
const roundService = createRoundService({
|
|
162
|
+
transcripts
|
|
163
|
+
});
|
|
149
164
|
const translationService = createTranslationService({
|
|
150
165
|
runtime,
|
|
151
166
|
sessionRegistry: registry,
|
|
152
|
-
transcripts
|
|
167
|
+
transcripts,
|
|
153
168
|
sessionService,
|
|
154
169
|
fs,
|
|
155
170
|
projectService,
|
|
@@ -165,6 +180,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
165
180
|
harnessService,
|
|
166
181
|
commandDispatcher,
|
|
167
182
|
messageService,
|
|
183
|
+
roundService,
|
|
168
184
|
statusService,
|
|
169
185
|
translationService,
|
|
170
186
|
runtime
|
|
@@ -180,7 +180,8 @@ function normalizeSettingsFile(input) {
|
|
|
180
180
|
function normalizePreferences(input) {
|
|
181
181
|
const candidate = isObject(input) ? input : {};
|
|
182
182
|
return {
|
|
183
|
-
themeMode: normalizeThemeMode(candidate.themeMode)
|
|
183
|
+
themeMode: normalizeThemeMode(candidate.themeMode),
|
|
184
|
+
roundCompletionAlerts: candidate.roundCompletionAlerts !== false
|
|
184
185
|
};
|
|
185
186
|
}
|
|
186
187
|
function normalizeThemeMode(input) {
|
|
@@ -11,55 +11,128 @@ const ROLE_TO_PM_TYPES = new Set(["result", "question", "blocked", "finding"]);
|
|
|
11
11
|
export function createMessageService(deps) {
|
|
12
12
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
13
13
|
const id = deps.id ?? (() => `msg_${randomUUID()}`);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
|
|
20
|
-
validateMessagePolicy(input.fromRole, input.toRole, input.type);
|
|
21
|
-
const timestamp = now();
|
|
22
|
-
const message = {
|
|
23
|
-
id: id(),
|
|
14
|
+
const taskLocks = new Map();
|
|
15
|
+
async function getOrchestrationState(input) {
|
|
16
|
+
const statePath = getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
|
|
17
|
+
if (!(await deps.fs.pathExists(statePath))) {
|
|
18
|
+
return {
|
|
24
19
|
taskSlug: input.taskSlug,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
body: input.body,
|
|
29
|
-
artifactRefs: input.artifactRefs ?? [],
|
|
30
|
-
parentMessageId: input.parentMessageId,
|
|
31
|
-
status: "queued",
|
|
32
|
-
createdAt: timestamp
|
|
20
|
+
mode: "manual",
|
|
21
|
+
paused: false,
|
|
22
|
+
updatedAt: now()
|
|
33
23
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
24
|
+
}
|
|
25
|
+
return deps.fs.readJson(statePath);
|
|
26
|
+
}
|
|
27
|
+
async function sendMessageLocked(input) {
|
|
28
|
+
await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
|
|
29
|
+
validateMessagePolicy(input.fromRole, input.toRole, input.type);
|
|
30
|
+
const timestamp = now();
|
|
31
|
+
const message = {
|
|
32
|
+
id: id(),
|
|
33
|
+
taskSlug: input.taskSlug,
|
|
34
|
+
fromRole: input.fromRole,
|
|
35
|
+
toRole: input.toRole,
|
|
36
|
+
type: input.type,
|
|
37
|
+
body: input.body,
|
|
38
|
+
artifactRefs: input.artifactRefs ?? [],
|
|
39
|
+
parentMessageId: input.parentMessageId,
|
|
40
|
+
status: "queued",
|
|
41
|
+
createdAt: timestamp
|
|
42
|
+
};
|
|
43
|
+
message.bodyPath = await writeMessageBody(deps.fs, input.taskRepoRoot ?? input.repoRoot, input.handoffDir, message);
|
|
44
|
+
let messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
45
|
+
if (isRoleActor(input.fromRole)) {
|
|
46
|
+
const acknowledged = acknowledgeActiveMessages(messages, input.fromRole, timestamp);
|
|
47
|
+
for (const acknowledgedMessage of acknowledged) {
|
|
48
|
+
await appendMessageSnapshot(deps.fs, input, acknowledgedMessage);
|
|
53
49
|
}
|
|
54
|
-
|
|
50
|
+
messages = applyMessageSnapshots(messages, acknowledged);
|
|
51
|
+
}
|
|
52
|
+
const state = await getOrchestrationState(input);
|
|
53
|
+
const result = await routeOutboundMessage(input, message, messages, state, timestamp);
|
|
54
|
+
await appendMessageSnapshot(deps.fs, input, result.message);
|
|
55
|
+
if (isRoleActor(input.fromRole)) {
|
|
56
|
+
await deliverNextQueuedMessage(input, input.fromRole);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
async function routeOutboundMessage(input, message, existingMessages, state, timestamp) {
|
|
61
|
+
const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.toRole);
|
|
62
|
+
if (!session || session.status !== "running") {
|
|
63
|
+
const queued = {
|
|
55
64
|
...message,
|
|
56
|
-
status: "
|
|
57
|
-
|
|
58
|
-
failureReason: undefined
|
|
65
|
+
status: "queued",
|
|
66
|
+
failureReason: `${input.toRole} session is not running.`
|
|
59
67
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
return { message: queued, delivered: false, requiresUserApproval: false };
|
|
69
|
+
}
|
|
70
|
+
if (state.mode === "manual") {
|
|
71
|
+
const pending = {
|
|
72
|
+
...message,
|
|
73
|
+
status: "pending_approval"
|
|
74
|
+
};
|
|
75
|
+
return { message: pending, delivered: false, requiresUserApproval: true };
|
|
76
|
+
}
|
|
77
|
+
if (state.paused) {
|
|
78
|
+
const queued = {
|
|
79
|
+
...message,
|
|
80
|
+
status: "queued",
|
|
81
|
+
failureReason: "Auto orchestration is paused."
|
|
82
|
+
};
|
|
83
|
+
return { message: queued, delivered: false, requiresUserApproval: false };
|
|
84
|
+
}
|
|
85
|
+
const activeMessage = findActiveMessageForRole(existingMessages, input.toRole);
|
|
86
|
+
if (activeMessage) {
|
|
87
|
+
const queued = {
|
|
88
|
+
...message,
|
|
89
|
+
status: "queued",
|
|
90
|
+
queuedBehindMessageId: activeMessage.id,
|
|
91
|
+
failureReason: `${input.toRole} is still handling VCM message ${activeMessage.id}.`
|
|
92
|
+
};
|
|
93
|
+
return { message: queued, delivered: false, requiresUserApproval: false };
|
|
94
|
+
}
|
|
95
|
+
const delivered = await deliverMessageToSession(session.id, message, timestamp);
|
|
96
|
+
return { message: delivered, delivered: true, requiresUserApproval: false };
|
|
97
|
+
}
|
|
98
|
+
async function deliverNextQueuedMessage(input, targetRole) {
|
|
99
|
+
const state = await getOrchestrationState(input);
|
|
100
|
+
if (state.mode !== "auto" || state.paused) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
104
|
+
if (findActiveMessageForRole(messages, targetRole)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const nextMessage = messages.find((message) => message.toRole === targetRole && message.status === "queued");
|
|
108
|
+
if (!nextMessage) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, targetRole);
|
|
112
|
+
if (!session || session.status !== "running") {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const delivered = await deliverMessageToSession(session.id, nextMessage, now());
|
|
116
|
+
await appendMessageSnapshot(deps.fs, input, delivered);
|
|
117
|
+
return delivered;
|
|
118
|
+
}
|
|
119
|
+
async function deliverMessageToSession(sessionId, message, timestamp) {
|
|
120
|
+
const delivered = {
|
|
121
|
+
...message,
|
|
122
|
+
status: "delivered",
|
|
123
|
+
deliveredAt: timestamp,
|
|
124
|
+
queuedBehindMessageId: undefined,
|
|
125
|
+
failureReason: undefined
|
|
126
|
+
};
|
|
127
|
+
await submitTerminalInput(deps.runtime, sessionId, renderMessageEnvelope(delivered));
|
|
128
|
+
return delivered;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
listMessages(input) {
|
|
132
|
+
return readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
133
|
+
},
|
|
134
|
+
async sendMessage(input) {
|
|
135
|
+
return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => sendMessageLocked(input));
|
|
63
136
|
},
|
|
64
137
|
async stageMessage(input) {
|
|
65
138
|
const message = await getMessageOrThrow(deps.fs, input);
|
|
@@ -96,19 +169,10 @@ export function createMessageService(deps) {
|
|
|
96
169
|
return rejected;
|
|
97
170
|
},
|
|
98
171
|
async getOrchestrationState(input) {
|
|
99
|
-
|
|
100
|
-
if (!(await deps.fs.pathExists(statePath))) {
|
|
101
|
-
return {
|
|
102
|
-
taskSlug: input.taskSlug,
|
|
103
|
-
mode: "manual",
|
|
104
|
-
paused: false,
|
|
105
|
-
updatedAt: now()
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
return deps.fs.readJson(statePath);
|
|
172
|
+
return getOrchestrationState(input);
|
|
109
173
|
},
|
|
110
174
|
async updateOrchestrationState(input) {
|
|
111
|
-
const current = await
|
|
175
|
+
const current = await getOrchestrationState(input);
|
|
112
176
|
const next = {
|
|
113
177
|
...current,
|
|
114
178
|
mode: input.mode ?? current.mode,
|
|
@@ -158,6 +222,45 @@ function validateMessagePolicy(fromRole, toRole, type) {
|
|
|
158
222
|
hint: "Use project-manager as the orchestration hub."
|
|
159
223
|
});
|
|
160
224
|
}
|
|
225
|
+
async function withTaskLock(locks, key, run) {
|
|
226
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
227
|
+
const next = previous.catch(() => undefined).then(run);
|
|
228
|
+
locks.set(key, next);
|
|
229
|
+
try {
|
|
230
|
+
return await next;
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
if (locks.get(key) === next) {
|
|
234
|
+
locks.delete(key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function isRoleActor(actor) {
|
|
239
|
+
return ROLE_NAMES.includes(actor);
|
|
240
|
+
}
|
|
241
|
+
function findActiveMessageForRole(messages, role) {
|
|
242
|
+
return messages.find((message) => message.toRole === role && message.status === "delivered");
|
|
243
|
+
}
|
|
244
|
+
function acknowledgeActiveMessages(messages, role, timestamp) {
|
|
245
|
+
return messages
|
|
246
|
+
.filter((message) => message.toRole === role && message.status === "delivered")
|
|
247
|
+
.map((message) => ({
|
|
248
|
+
...message,
|
|
249
|
+
status: "acknowledged",
|
|
250
|
+
acknowledgedAt: timestamp,
|
|
251
|
+
failureReason: undefined
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
function applyMessageSnapshots(messages, snapshots) {
|
|
255
|
+
if (snapshots.length === 0) {
|
|
256
|
+
return messages;
|
|
257
|
+
}
|
|
258
|
+
const latest = new Map(messages.map((message) => [message.id, message]));
|
|
259
|
+
for (const snapshot of snapshots) {
|
|
260
|
+
latest.set(snapshot.id, snapshot);
|
|
261
|
+
}
|
|
262
|
+
return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
263
|
+
}
|
|
161
264
|
async function writeMessageBody(fs, repoRoot, handoffDir, message) {
|
|
162
265
|
const bodyPath = path.posix.join(handoffDir, "messages", `${message.id}.md`);
|
|
163
266
|
await fs.writeText(resolveRepoPath(repoRoot, bodyPath), renderMessageBodyFile(message));
|
|
@@ -31,7 +31,7 @@ export function createProjectService(deps) {
|
|
|
31
31
|
const config = await this.loadConfig(repoRoot);
|
|
32
32
|
await deps.fs.ensureDir(path.join(repoRoot, config.handoffRoot));
|
|
33
33
|
await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "tasks"));
|
|
34
|
-
await deps.fs.ensureDir(path.join(repoRoot,
|
|
34
|
+
await deps.fs.ensureDir(path.join(repoRoot, ".claude", "worktrees"));
|
|
35
35
|
await this.saveConfig(config, true);
|
|
36
36
|
const warnings = [];
|
|
37
37
|
let branch = "unknown";
|