@devcxl/opencode-cabbage 0.3.2 → 1.1.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/README.md +34 -19
- package/README.zh.md +34 -19
- package/assets/agents/dev-lifecycle.md +60 -49
- package/assets/agents/team/architect.md +8 -1
- package/assets/agents/team/developer.md +89 -0
- package/assets/agents/team/goal-verify.md +5 -1
- package/assets/agents/team/reviewer.md +6 -0
- package/assets/commands/code.md +3 -2
- package/assets/commands/design.md +3 -2
- package/assets/commands/release.md +3 -2
- package/assets/commands/requirements.md +3 -2
- package/assets/commands/review.md +3 -2
- package/assets/commands/setup.md +3 -2
- package/assets/commands/tasks.md +2 -1
- package/assets/prompts/bootstrap.md +9 -8
- package/assets/prompts/stage-contract.md +55 -0
- package/assets/skills/flow-code/SKILL.md +38 -140
- package/assets/skills/flow-design/SKILL.md +28 -79
- package/assets/skills/flow-release/SKILL.md +31 -51
- package/assets/skills/flow-requirements/SKILL.md +31 -196
- package/assets/skills/flow-review/SKILL.md +26 -123
- package/assets/skills/flow-setup/SKILL.md +40 -43
- package/assets/skills/flow-tasks/SKILL.md +23 -153
- package/assets/skills/flow-tdd/SKILL.md +30 -31
- package/dist/kernel/caller.d.ts +48 -0
- package/dist/kernel/caller.js +88 -0
- package/dist/kernel/caller.js.map +1 -0
- package/dist/kernel/context.d.ts +27 -0
- package/dist/kernel/context.js +84 -0
- package/dist/kernel/context.js.map +1 -0
- package/dist/kernel/flow.d.ts +175 -0
- package/dist/kernel/flow.js +467 -0
- package/dist/kernel/flow.js.map +1 -0
- package/dist/kernel/legacy.d.ts +19 -0
- package/dist/kernel/legacy.js +34 -0
- package/dist/kernel/legacy.js.map +1 -0
- package/dist/kernel/mutex.d.ts +8 -0
- package/dist/kernel/mutex.js +23 -0
- package/dist/kernel/mutex.js.map +1 -0
- package/dist/kernel/permission.d.ts +21 -0
- package/dist/kernel/permission.js +45 -0
- package/dist/kernel/permission.js.map +1 -0
- package/dist/kernel/profile.d.ts +35 -0
- package/dist/kernel/profile.js +147 -0
- package/dist/kernel/profile.js.map +1 -0
- package/dist/kernel/records.d.ts +103 -0
- package/dist/kernel/records.js +240 -0
- package/dist/kernel/records.js.map +1 -0
- package/dist/kernel/release.d.ts +106 -0
- package/dist/kernel/release.js +241 -0
- package/dist/kernel/release.js.map +1 -0
- package/dist/kernel/review.d.ts +34 -0
- package/dist/kernel/review.js +82 -0
- package/dist/kernel/review.js.map +1 -0
- package/dist/kernel/session-index.d.ts +45 -0
- package/dist/kernel/session-index.js +136 -0
- package/dist/kernel/session-index.js.map +1 -0
- package/dist/kernel/setup.d.ts +95 -0
- package/dist/kernel/setup.js +401 -0
- package/dist/kernel/setup.js.map +1 -0
- package/dist/kernel/slug.d.ts +23 -0
- package/dist/kernel/slug.js +67 -0
- package/dist/kernel/slug.js.map +1 -0
- package/dist/kernel/task.d.ts +213 -0
- package/dist/kernel/task.js +873 -0
- package/dist/kernel/task.js.map +1 -0
- package/dist/{flowrun → kernel/tdd}/adapter.d.ts +1 -1
- package/dist/{flowrun → kernel/tdd}/adapter.js +27 -26
- package/dist/kernel/tdd/adapter.js.map +1 -0
- package/dist/{flowrun → kernel/tdd}/digest.d.ts +2 -1
- package/dist/{flowrun → kernel/tdd}/digest.js +6 -4
- package/dist/kernel/tdd/digest.js.map +1 -0
- package/dist/{flowrun → kernel/tdd}/evaluator.d.ts +1 -1
- package/dist/{flowrun → kernel/tdd}/evaluator.js +1 -1
- package/dist/kernel/tdd/evaluator.js.map +1 -0
- package/dist/kernel/tdd/evidence.d.ts +42 -0
- package/dist/kernel/tdd/evidence.js +48 -0
- package/dist/kernel/tdd/evidence.js.map +1 -0
- package/dist/{flowrun → kernel/tdd}/state.d.ts +2 -2
- package/dist/{flowrun → kernel/tdd}/state.js +1 -1
- package/dist/kernel/tdd/state.js.map +1 -0
- package/dist/kernel/types.d.ts +180 -0
- package/dist/kernel/types.js +2 -0
- package/dist/kernel/types.js.map +1 -0
- package/dist/kernel/worktree.d.ts +45 -0
- package/dist/kernel/worktree.js +156 -0
- package/dist/kernel/worktree.js.map +1 -0
- package/dist/plugin/agents.d.ts +5 -17
- package/dist/plugin/agents.js +0 -23
- package/dist/plugin/agents.js.map +1 -1
- package/dist/plugin/flow-control.d.ts +91 -0
- package/dist/plugin/flow-control.js +287 -0
- package/dist/plugin/flow-control.js.map +1 -0
- package/dist/plugin/goal.d.ts +17 -46
- package/dist/plugin/goal.js +47 -117
- package/dist/plugin/goal.js.map +1 -1
- package/dist/plugin/prompt-lint.js +78 -6
- package/dist/plugin/prompt-lint.js.map +1 -1
- package/dist/plugin/release-control.d.ts +39 -0
- package/dist/plugin/release-control.js +343 -0
- package/dist/plugin/release-control.js.map +1 -0
- package/dist/plugin/server.d.ts +19 -0
- package/dist/plugin/server.js +198 -320
- package/dist/plugin/server.js.map +1 -1
- package/dist/plugin/setup-control.d.ts +59 -0
- package/dist/plugin/setup-control.js +111 -0
- package/dist/plugin/setup-control.js.map +1 -0
- package/dist/plugin/shell.d.ts +7 -34
- package/dist/plugin/shell.js +12 -137
- package/dist/plugin/shell.js.map +1 -1
- package/dist/plugin/skills.d.ts +1 -1
- package/dist/plugin/skills.js +1 -7
- package/dist/plugin/skills.js.map +1 -1
- package/dist/plugin/task-control.d.ts +90 -0
- package/dist/plugin/task-control.js +208 -0
- package/dist/plugin/task-control.js.map +1 -0
- package/dist/plugin/tdd-checkpoint.d.ts +128 -0
- package/dist/plugin/tdd-checkpoint.js +664 -0
- package/dist/plugin/tdd-checkpoint.js.map +1 -0
- package/dist/util/gh.d.ts +5 -0
- package/dist/util/gh.js +12 -0
- package/dist/util/gh.js.map +1 -1
- package/dist/util/paths.d.ts +6 -2
- package/dist/util/paths.js +15 -7
- package/dist/util/paths.js.map +1 -1
- package/package.json +4 -4
- package/assets/agents/team/backend.md +0 -100
- package/assets/agents/team/frontend.md +0 -98
- package/assets/commands/handoff.md +0 -4
- package/assets/commands/test.md +0 -4
- package/assets/context/CONTEXT.md +0 -41
- package/assets/skills/flow-handoff/SKILL.md +0 -76
- package/assets/skills/flow-test/SKILL.md +0 -75
- package/dist/flowrun/adapter.js.map +0 -1
- package/dist/flowrun/audit.d.ts +0 -5
- package/dist/flowrun/audit.js +0 -67
- package/dist/flowrun/audit.js.map +0 -1
- package/dist/flowrun/ci.d.ts +0 -64
- package/dist/flowrun/ci.js +0 -184
- package/dist/flowrun/ci.js.map +0 -1
- package/dist/flowrun/coverage.d.ts +0 -70
- package/dist/flowrun/coverage.js +0 -207
- package/dist/flowrun/coverage.js.map +0 -1
- package/dist/flowrun/digest.js.map +0 -1
- package/dist/flowrun/evaluator.js.map +0 -1
- package/dist/flowrun/gate.d.ts +0 -28
- package/dist/flowrun/gate.js +0 -139
- package/dist/flowrun/gate.js.map +0 -1
- package/dist/flowrun/github.d.ts +0 -24
- package/dist/flowrun/github.js +0 -154
- package/dist/flowrun/github.js.map +0 -1
- package/dist/flowrun/index.d.ts +0 -14
- package/dist/flowrun/index.js +0 -15
- package/dist/flowrun/index.js.map +0 -1
- package/dist/flowrun/merge.d.ts +0 -57
- package/dist/flowrun/merge.js +0 -180
- package/dist/flowrun/merge.js.map +0 -1
- package/dist/flowrun/migration.d.ts +0 -11
- package/dist/flowrun/migration.js +0 -276
- package/dist/flowrun/migration.js.map +0 -1
- package/dist/flowrun/resilience.d.ts +0 -18
- package/dist/flowrun/resilience.js +0 -80
- package/dist/flowrun/resilience.js.map +0 -1
- package/dist/flowrun/state.js.map +0 -1
- package/dist/flowrun/transitions.d.ts +0 -66
- package/dist/flowrun/transitions.js +0 -168
- package/dist/flowrun/transitions.js.map +0 -1
- package/dist/flowrun/types.d.ts +0 -356
- package/dist/flowrun/types.js +0 -21
- package/dist/flowrun/types.js.map +0 -1
- package/dist/flowrun/validator.d.ts +0 -19
- package/dist/flowrun/validator.js +0 -209
- package/dist/flowrun/validator.js.map +0 -1
- package/dist/plugin/broker.d.ts +0 -73
- package/dist/plugin/broker.js +0 -140
- package/dist/plugin/broker.js.map +0 -1
- package/dist/plugin/tdd-tool.d.ts +0 -62
- package/dist/plugin/tdd-tool.js +0 -336
- package/dist/plugin/tdd-tool.js.map +0 -1
package/dist/plugin/server.js
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
2
|
import { initPrompts } from "./prompts.js";
|
|
6
3
|
import { initBootstrap, getBootstrapContent } from "./bootstrap.js";
|
|
7
4
|
import { loadCommands } from "./commands.js";
|
|
8
5
|
import { setupSkillsDir } from "./skills.js";
|
|
9
6
|
import { loadAgents } from "./agents.js";
|
|
10
|
-
import {
|
|
11
|
-
import { createGoalClient, createGoalTool, readGoal, writeGoal,
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
7
|
+
import { createAgentShellEnv } from "./shell.js";
|
|
8
|
+
import { createGoalClient, createGoalTool, readGoal, writeGoal, MAX_CONTINUATIONS, continuationPrompt, formatGoal } from "./goal.js";
|
|
9
|
+
import { readIndex, updateFlowSession } from "../kernel/session-index.js";
|
|
10
|
+
import { createSetupControlTool } from "./setup-control.js";
|
|
11
|
+
import { createFlowControlTool } from "./flow-control.js";
|
|
12
|
+
import { createTaskControlTool } from "./task-control.js";
|
|
13
|
+
import { createTddCheckpointTool } from "./tdd-checkpoint.js";
|
|
14
|
+
import { createReleaseControlTool } from "./release-control.js";
|
|
15
|
+
import { getContextBlock, formatContextBlock, CONTEXT_MARKER } from "../kernel/context.js";
|
|
16
|
+
import { readProjectProfile } from "../kernel/profile.js";
|
|
15
17
|
const abortedSessions = new Set();
|
|
16
18
|
const errorRetryCount = new Map();
|
|
17
19
|
const COMPACTION_THRESHOLD = 20;
|
|
20
|
+
/**
|
|
21
|
+
* 决定首条 user 消息注入内容:
|
|
22
|
+
* - Primary(goal 激活)→ bootstrap + Project Context 块
|
|
23
|
+
* - 子 agent → 仅 Project Context 块
|
|
24
|
+
* - 其他 → 不注入
|
|
25
|
+
*/
|
|
26
|
+
export function buildFirstUserInjection(input) {
|
|
27
|
+
const parts = [];
|
|
28
|
+
if (input.hasGoal)
|
|
29
|
+
parts.push(input.bootstrap);
|
|
30
|
+
if (input.contextBlock && (input.hasGoal || input.isSubAgent)) {
|
|
31
|
+
parts.push(input.contextBlock.block);
|
|
32
|
+
}
|
|
33
|
+
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
34
|
+
}
|
|
18
35
|
export function configureGoalTools(config) {
|
|
19
36
|
config.tools = { ...config.tools, goal: false };
|
|
20
37
|
for (const [agentName, agent] of Object.entries(config.agent ?? {})) {
|
|
@@ -25,57 +42,68 @@ export function configureGoalTools(config) {
|
|
|
25
42
|
agentConfig.tools = { ...agentConfig.tools, goal: canUseGoal };
|
|
26
43
|
}
|
|
27
44
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
/**
|
|
46
|
+
* config 层第二道门(spec §2.2):按 caller 矩阵把 5 个生命周期工具
|
|
47
|
+
* 对每个 agent 置 true/false。agent 名 → 角色映射与 caller.ts ROLE_BY_AGENT 一致。
|
|
48
|
+
*/
|
|
49
|
+
export function configureLifecycleTools(config) {
|
|
50
|
+
const lifecycleTools = ["setup_control", "flow_control", "task_control", "tdd_checkpoint", "release_control"];
|
|
51
|
+
const agentRole = (name) => {
|
|
52
|
+
switch (name) {
|
|
53
|
+
case "dev-lifecycle": return "primary";
|
|
54
|
+
case "developer": return "developer";
|
|
55
|
+
case "architect": return "architect";
|
|
56
|
+
case "reviewer": return "reviewer";
|
|
57
|
+
case "goal-verify": return "goal-verify";
|
|
58
|
+
default: return "reviewer";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const canUseTool = (role, tool) => {
|
|
62
|
+
if (role === "goal-verify")
|
|
63
|
+
return tool === "flow_control";
|
|
64
|
+
switch (tool) {
|
|
65
|
+
case "tdd_checkpoint": return role === "primary" || role === "developer";
|
|
66
|
+
default: return role === "primary";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
for (const [agentName, agent] of Object.entries(config.agent ?? {})) {
|
|
70
|
+
if (!agent || typeof agent !== "object" || Array.isArray(agent))
|
|
71
|
+
continue;
|
|
72
|
+
const agentConfig = agent;
|
|
73
|
+
const role = agentRole(agentName);
|
|
74
|
+
const toolFlags = {};
|
|
75
|
+
for (const toolName of lifecycleTools) {
|
|
76
|
+
toolFlags[toolName] = canUseTool(role, toolName);
|
|
77
|
+
}
|
|
78
|
+
agentConfig.tools = { ...agentConfig.tools, ...toolFlags };
|
|
53
79
|
}
|
|
54
|
-
catch { }
|
|
55
80
|
}
|
|
56
81
|
async function queueContinuation(client, sessionID, projectDir) {
|
|
57
82
|
const { goal } = await readGoal(client, sessionID);
|
|
58
83
|
if (!goal || goal.status !== "active")
|
|
59
84
|
return;
|
|
60
|
-
if (projectDir)
|
|
61
|
-
|
|
85
|
+
if (projectDir) {
|
|
86
|
+
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: goal.status, continuationCount: goal.continuationCount });
|
|
87
|
+
}
|
|
62
88
|
if (abortedSessions.has(sessionID)) {
|
|
63
89
|
abortedSessions.delete(sessionID);
|
|
64
90
|
goal.status = "paused";
|
|
65
91
|
await writeGoal(client, sessionID, goal);
|
|
66
92
|
if (projectDir)
|
|
67
|
-
|
|
93
|
+
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: "paused" });
|
|
68
94
|
return;
|
|
69
95
|
}
|
|
70
96
|
if (goal.continuationCount >= MAX_CONTINUATIONS) {
|
|
71
97
|
goal.status = "paused";
|
|
72
98
|
await writeGoal(client, sessionID, goal);
|
|
73
99
|
if (projectDir)
|
|
74
|
-
|
|
100
|
+
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: "paused" });
|
|
75
101
|
return;
|
|
76
102
|
}
|
|
77
103
|
goal.continuationCount++;
|
|
78
104
|
await writeGoal(client, sessionID, goal);
|
|
105
|
+
if (projectDir)
|
|
106
|
+
await updateFlowSession(projectDir, goal.parentIssueNumber, { continuationCount: goal.continuationCount });
|
|
79
107
|
try {
|
|
80
108
|
if (goal.continuationCount > 0 && goal.continuationCount % COMPACTION_THRESHOLD === 0) {
|
|
81
109
|
try {
|
|
@@ -86,9 +114,13 @@ async function queueContinuation(client, sessionID, projectDir) {
|
|
|
86
114
|
}
|
|
87
115
|
catch { }
|
|
88
116
|
}
|
|
117
|
+
const contextBlock = projectDir ? await getContextBlock(projectDir) : null;
|
|
118
|
+
const contextText = contextBlock ? `${formatContextBlock(contextBlock)}\n\n` : "";
|
|
119
|
+
// 显式传回用户最后使用的 agent/模型,防止 idle 续接回落/切换默认 agent(如 build)
|
|
89
120
|
await client.session.promptAsync({
|
|
90
121
|
sessionID,
|
|
91
|
-
parts: [{ type: "text", text: continuationPrompt(goal.
|
|
122
|
+
parts: [{ type: "text", text: contextText + continuationPrompt(goal.parentIssueNumber), synthetic: true }],
|
|
123
|
+
...(goal.sessionProfile ? { agent: goal.sessionProfile.agent, model: goal.sessionProfile.model } : {}),
|
|
92
124
|
});
|
|
93
125
|
}
|
|
94
126
|
catch (err) {
|
|
@@ -96,26 +128,29 @@ async function queueContinuation(client, sessionID, projectDir) {
|
|
|
96
128
|
}
|
|
97
129
|
}
|
|
98
130
|
async function autoResume(client, projectDir) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
const index = await readIndex(projectDir);
|
|
132
|
+
for (const [parentIssueNumber, entry] of Object.entries(index.flows)) {
|
|
133
|
+
if (entry.status !== "active")
|
|
134
|
+
continue;
|
|
135
|
+
const { goal } = await readGoal(client, entry.sessionID);
|
|
136
|
+
if (!goal || goal.status !== "active") {
|
|
137
|
+
await updateFlowSession(projectDir, Number(parentIssueNumber), { status: "paused" });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await client.session.promptAsync({
|
|
142
|
+
sessionID: entry.sessionID,
|
|
143
|
+
parts: [{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: `[auto-resume] Plugin restarted. Resuming previous goal:\n\n${formatGoal(goal)}\n\nContinue working.`,
|
|
146
|
+
synthetic: true,
|
|
147
|
+
}],
|
|
148
|
+
...(goal.sessionProfile ? { agent: goal.sessionProfile.agent, model: goal.sessionProfile.model } : {}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
console.error("[cabbage] auto-resume failed:", err);
|
|
153
|
+
}
|
|
119
154
|
}
|
|
120
155
|
}
|
|
121
156
|
function getRetryKey(sessionID, phase) {
|
|
@@ -141,257 +176,40 @@ function startPeriodicCleanup() {
|
|
|
141
176
|
errorRetryCount.clear();
|
|
142
177
|
}, ABORTED_SESSION_CLEANUP_INTERVAL);
|
|
143
178
|
}
|
|
144
|
-
// ─── Flow Control Tool ───
|
|
145
|
-
function createFlowControlTool(broker, goalClient) {
|
|
146
|
-
return tool({
|
|
147
|
-
description: `Control the FlowRun lifecycle: start a FlowRun, transition stages, start tasks, and finalize completed runs.
|
|
148
|
-
|
|
149
|
-
Operations:
|
|
150
|
-
- run-start: Transition FlowRun from planned → running. Binds Goal to FlowRun.
|
|
151
|
-
- stage-start: Start a stage (requirements/design/tasks/code). Requires prerequisites met.
|
|
152
|
-
- stage-complete: Complete a stage (requirements/design/tasks). Requires all checks pass.
|
|
153
|
-
- task-start: Start a task (pending/ready → running). Freezes TDD policy and sets execution binding.
|
|
154
|
-
- run-finalize: Mark code→test→review→merge stages as pass, set FlowRun to completed. Requires all Tasks merged. Idempotent.`,
|
|
155
|
-
args: {
|
|
156
|
-
op: tool.schema.enum(["run-start", "stage-start", "stage-complete", "task-start", "run-finalize"]).describe("Flow control operation"),
|
|
157
|
-
parent_issue_number: tool.schema.number().describe("Parent GitHub Issue number containing the FlowRun"),
|
|
158
|
-
stage: tool.schema.enum(["requirements", "design", "tasks", "code", "test", "review", "merge"]).optional().describe("Stage name (for stage-start/stage-complete)"),
|
|
159
|
-
task_id: tool.schema.string().optional().describe("Task ID (for task-start)"),
|
|
160
|
-
execution_binding: tool.schema.object({
|
|
161
|
-
branch: tool.schema.string(),
|
|
162
|
-
base_sha: tool.schema.string(),
|
|
163
|
-
start_head_sha: tool.schema.string(),
|
|
164
|
-
worktree_id: tool.schema.string(),
|
|
165
|
-
session_id: tool.schema.string(),
|
|
166
|
-
}).optional().describe("Execution binding for task-start"),
|
|
167
|
-
tdd_policy_json: tool.schema.string().optional().describe("JSON string of frozen TDD policy (for task-start)"),
|
|
168
|
-
},
|
|
169
|
-
async execute(args, ctx) {
|
|
170
|
-
const sessionID = ctx.sessionID;
|
|
171
|
-
const op = args.op;
|
|
172
|
-
const parentIssueNumber = args.parent_issue_number;
|
|
173
|
-
try {
|
|
174
|
-
switch (op) {
|
|
175
|
-
case "run-start":
|
|
176
|
-
return handleRunStart(broker, goalClient, parentIssueNumber, sessionID);
|
|
177
|
-
case "stage-start":
|
|
178
|
-
return handleStageStart(broker, parentIssueNumber, args.stage);
|
|
179
|
-
case "stage-complete":
|
|
180
|
-
return handleStageComplete(broker, parentIssueNumber, args.stage);
|
|
181
|
-
case "task-start":
|
|
182
|
-
return handleTaskStart(broker, parentIssueNumber, args.task_id, args.execution_binding, args.tdd_policy_json);
|
|
183
|
-
case "run-finalize":
|
|
184
|
-
return handleRunFinalize(broker, parentIssueNumber);
|
|
185
|
-
default:
|
|
186
|
-
return errorResponse("UNKNOWN_OP", `Unknown operation: "${op}"`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch (err) {
|
|
190
|
-
return errorResponse("INTERNAL_ERROR", String(err));
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
function okResponse(overrides = {}) {
|
|
196
|
-
const resp = { ok: true, ...overrides };
|
|
197
|
-
return JSON.stringify(resp, null, 2);
|
|
198
|
-
}
|
|
199
|
-
function errorResponse(code, message) {
|
|
200
|
-
const resp = { ok: false, error: { code, message } };
|
|
201
|
-
return JSON.stringify(resp, null, 2);
|
|
202
|
-
}
|
|
203
|
-
async function handleRunStart(broker, goalClient, parentIssueNumber, sessionID) {
|
|
204
|
-
// 通过 broker 执行状态迁移
|
|
205
|
-
const writeResult = await broker.writeFlowRunWithLock(parentIssueNumber, (flowRun) => {
|
|
206
|
-
const res = flowRunStart(flowRun);
|
|
207
|
-
if (!res.ok) {
|
|
208
|
-
return { flowRun, result: res.error, shouldPersist: false };
|
|
209
|
-
}
|
|
210
|
-
return { flowRun: res.value, result: res.value.status, shouldPersist: true };
|
|
211
|
-
});
|
|
212
|
-
if (!writeResult.ok) {
|
|
213
|
-
return errorResponse(writeResult.code, writeResult.message);
|
|
214
|
-
}
|
|
215
|
-
if (!writeResult.persisted) {
|
|
216
|
-
// 状态迁移被拒绝(如已是 running)
|
|
217
|
-
return errorResponse("INVALID_TRANSITION", "FlowRun is not in planned state");
|
|
218
|
-
}
|
|
219
|
-
// 绑定 Goal → FlowRun
|
|
220
|
-
const actualFlowRunId = writeResult.flowRun.flowRunId;
|
|
221
|
-
const bindResult = await bindFlowRunRef(goalClient, sessionID, {
|
|
222
|
-
repo: writeResult.flowRun.repo,
|
|
223
|
-
parentIssueNumber,
|
|
224
|
-
flowRunId: actualFlowRunId,
|
|
225
|
-
});
|
|
226
|
-
if (!bindResult.ok) {
|
|
227
|
-
return errorResponse(bindResult.code, bindResult.message);
|
|
228
|
-
}
|
|
229
|
-
return okResponse({
|
|
230
|
-
flowRunStatus: writeResult.flowRun.status,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
async function handleStageStart(broker, parentIssueNumber, stageName) {
|
|
234
|
-
if (!stageName) {
|
|
235
|
-
return errorResponse("POLICY_INVALID", "stage is required for stage-start");
|
|
236
|
-
}
|
|
237
|
-
const writeResult = await broker.writeFlowRunWithLock(parentIssueNumber, (flowRun) => {
|
|
238
|
-
const res = flowStageStart(flowRun, stageName);
|
|
239
|
-
if (!res.ok) {
|
|
240
|
-
return { flowRun, result: res.error, shouldPersist: false };
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
flowRun: res.value,
|
|
244
|
-
result: { stage: stageName, status: res.value.stages[stageName].status },
|
|
245
|
-
shouldPersist: true,
|
|
246
|
-
};
|
|
247
|
-
});
|
|
248
|
-
if (!writeResult.ok) {
|
|
249
|
-
return errorResponse(writeResult.code, writeResult.message);
|
|
250
|
-
}
|
|
251
|
-
if (!writeResult.persisted && writeResult.result && typeof writeResult.result === "object" && "code" in writeResult.result) {
|
|
252
|
-
const err = writeResult.result;
|
|
253
|
-
return errorResponse(err.code, err.message);
|
|
254
|
-
}
|
|
255
|
-
return okResponse({
|
|
256
|
-
flowRunStatus: writeResult.flowRun.status,
|
|
257
|
-
stage: writeResult.result,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
async function handleStageComplete(broker, parentIssueNumber, stageName) {
|
|
261
|
-
if (!stageName) {
|
|
262
|
-
return errorResponse("POLICY_INVALID", "stage is required for stage-complete");
|
|
263
|
-
}
|
|
264
|
-
const writeResult = await broker.writeFlowRunWithLock(parentIssueNumber, (flowRun) => {
|
|
265
|
-
const res = flowStageComplete(flowRun, stageName);
|
|
266
|
-
if (!res.ok) {
|
|
267
|
-
return { flowRun, result: res.error, shouldPersist: false };
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
flowRun: res.value,
|
|
271
|
-
result: { stage: stageName, status: res.value.stages[stageName].status },
|
|
272
|
-
shouldPersist: true,
|
|
273
|
-
};
|
|
274
|
-
});
|
|
275
|
-
if (!writeResult.ok) {
|
|
276
|
-
return errorResponse(writeResult.code, writeResult.message);
|
|
277
|
-
}
|
|
278
|
-
if (!writeResult.persisted && writeResult.result && typeof writeResult.result === "object" && "code" in writeResult.result) {
|
|
279
|
-
const err = writeResult.result;
|
|
280
|
-
return errorResponse(err.code, err.message);
|
|
281
|
-
}
|
|
282
|
-
return okResponse({
|
|
283
|
-
flowRunStatus: writeResult.flowRun.status,
|
|
284
|
-
stage: writeResult.result,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
async function handleTaskStart(broker, parentIssueNumber, taskId, executionBinding, tddPolicyJson) {
|
|
288
|
-
if (!taskId) {
|
|
289
|
-
return errorResponse("POLICY_INVALID", "task_id is required for task-start");
|
|
290
|
-
}
|
|
291
|
-
if (!executionBinding) {
|
|
292
|
-
return errorResponse("POLICY_INVALID", "execution_binding is required for task-start");
|
|
293
|
-
}
|
|
294
|
-
// 解析 frozen policy
|
|
295
|
-
let tddPolicy = undefined;
|
|
296
|
-
if (tddPolicyJson) {
|
|
297
|
-
try {
|
|
298
|
-
tddPolicy = JSON.parse(tddPolicyJson);
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
return errorResponse("POLICY_INVALID", "tdd_policy_json is not valid JSON");
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const writeResult = await broker.writeFlowRunWithLock(parentIssueNumber, (flowRun) => {
|
|
305
|
-
const res = flowTaskStart(flowRun, taskId, executionBinding, tddPolicy);
|
|
306
|
-
if (!res.ok) {
|
|
307
|
-
return { flowRun, result: res.error, shouldPersist: false };
|
|
308
|
-
}
|
|
309
|
-
return {
|
|
310
|
-
flowRun: res.value.flowRun,
|
|
311
|
-
result: {
|
|
312
|
-
taskId,
|
|
313
|
-
status: res.value.task.status,
|
|
314
|
-
executionBinding: res.value.task.executionBinding,
|
|
315
|
-
},
|
|
316
|
-
shouldPersist: true,
|
|
317
|
-
};
|
|
318
|
-
});
|
|
319
|
-
if (!writeResult.ok) {
|
|
320
|
-
return errorResponse(writeResult.code, writeResult.message);
|
|
321
|
-
}
|
|
322
|
-
if (!writeResult.persisted && writeResult.result && typeof writeResult.result === "object" && "code" in writeResult.result) {
|
|
323
|
-
const err = writeResult.result;
|
|
324
|
-
return errorResponse(err.code, err.message);
|
|
325
|
-
}
|
|
326
|
-
return okResponse({
|
|
327
|
-
flowRunStatus: writeResult.flowRun.status,
|
|
328
|
-
task: writeResult.result,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
async function handleRunFinalize(broker, parentIssueNumber) {
|
|
332
|
-
const writeResult = await broker.writeFlowRunWithLock(parentIssueNumber, (flowRun) => {
|
|
333
|
-
const res = flowRunFinalize(flowRun);
|
|
334
|
-
if (!res.ok) {
|
|
335
|
-
return { flowRun, result: res.error, shouldPersist: false };
|
|
336
|
-
}
|
|
337
|
-
return {
|
|
338
|
-
flowRun: res.value,
|
|
339
|
-
result: { status: res.value.status, completedAt: res.value.completedAt },
|
|
340
|
-
shouldPersist: true,
|
|
341
|
-
};
|
|
342
|
-
});
|
|
343
|
-
if (!writeResult.ok) {
|
|
344
|
-
return errorResponse(writeResult.code, writeResult.message);
|
|
345
|
-
}
|
|
346
|
-
if (!writeResult.persisted && writeResult.result && typeof writeResult.result === "object" && "code" in writeResult.result) {
|
|
347
|
-
const err = writeResult.result;
|
|
348
|
-
return errorResponse(err.code, err.message);
|
|
349
|
-
}
|
|
350
|
-
return okResponse({
|
|
351
|
-
flowRunStatus: writeResult.flowRun.status,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
179
|
export function createOpencodeCabbage(packageRoot) {
|
|
355
180
|
return async (ctx, _options) => {
|
|
356
181
|
const sourceSkillsDir = path.join(packageRoot, "assets", "skills");
|
|
357
|
-
const contextDir = path.join(packageRoot, "assets", "context");
|
|
358
182
|
const promptsDir = path.join(packageRoot, "assets", "prompts");
|
|
359
183
|
const commandsDir = path.join(packageRoot, "assets", "commands");
|
|
360
|
-
const skillsDir = await setupSkillsDir(sourceSkillsDir,
|
|
184
|
+
const skillsDir = await setupSkillsDir(sourceSkillsDir, promptsDir);
|
|
361
185
|
const projectDir = ctx.worktree || ctx.directory;
|
|
362
186
|
const v1Client = ctx.client._client;
|
|
363
187
|
const goalClient = createGoalClient(ctx.serverUrl, v1Client);
|
|
364
|
-
const goalTool = createGoalTool(goalClient
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (!flowResult.ok) {
|
|
372
|
-
return `Failed to read FlowRun #${ref.parentIssueNumber}: ${flowResult.code}`;
|
|
373
|
-
}
|
|
374
|
-
return checkFlowRunBlockers(flowResult.data.status);
|
|
375
|
-
});
|
|
376
|
-
const broker = new FlowBroker();
|
|
377
|
-
const flowControlTool = createFlowControlTool(broker, goalClient);
|
|
188
|
+
const goalTool = createGoalTool(goalClient);
|
|
189
|
+
const sessionClient = goalClient;
|
|
190
|
+
const flowControlTool = createFlowControlTool({ projectDir, sessionClient });
|
|
191
|
+
const taskControlTool = createTaskControlTool({ projectDir, sessionClient });
|
|
192
|
+
const setupControlTool = createSetupControlTool({ projectDir, sessionClient });
|
|
193
|
+
const tddCheckpointTool = createTddCheckpointTool({ projectDir, sessionClient });
|
|
194
|
+
const releaseControlTool = createReleaseControlTool({ projectDir, sessionClient });
|
|
378
195
|
const agentsDir = path.join(packageRoot, "assets", "agents");
|
|
379
196
|
await initPrompts(packageRoot, projectDir);
|
|
380
197
|
await initBootstrap();
|
|
381
198
|
startPeriodicCleanup();
|
|
382
|
-
autoResume(goalClient, projectDir)
|
|
199
|
+
void autoResume(goalClient, projectDir).catch(err => {
|
|
200
|
+
console.error("[cabbage] auto-resume failed:", err);
|
|
201
|
+
});
|
|
383
202
|
return {
|
|
384
203
|
tool: {
|
|
385
204
|
goal: goalTool,
|
|
386
205
|
flow_control: flowControlTool,
|
|
206
|
+
task_control: taskControlTool,
|
|
207
|
+
setup_control: setupControlTool,
|
|
208
|
+
tdd_checkpoint: tddCheckpointTool,
|
|
209
|
+
release_control: releaseControlTool,
|
|
387
210
|
},
|
|
388
211
|
config: async (rawConfig) => {
|
|
389
212
|
const config = rawConfig;
|
|
390
|
-
// ── Ambient credential 检测 ──
|
|
391
|
-
const ambientReport = detectAmbientCredentials();
|
|
392
|
-
if (ambientReport.hasWriteCredentials) {
|
|
393
|
-
console.warn("[cabbage] ⚠️ 检测到可用 GitHub 写凭证,Runtime enforcement 已降级为 advisory:", ambientReport.sources.map(s => s.location).join(", "));
|
|
394
|
-
}
|
|
395
213
|
config.skills = config.skills || {};
|
|
396
214
|
config.skills.paths = config.skills.paths || [];
|
|
397
215
|
if (!config.skills.paths.includes(skillsDir)) {
|
|
@@ -410,6 +228,31 @@ export function createOpencodeCabbage(packageRoot) {
|
|
|
410
228
|
};
|
|
411
229
|
}
|
|
412
230
|
config.agent = config.agent || {};
|
|
231
|
+
// 渲染 permission 占位符:`<profile-test-command>` → Profile.testCommand 首 token(§9.1)
|
|
232
|
+
const profile = await readProjectProfile(projectDir);
|
|
233
|
+
const testCommandToken = profile.testCommand?.split(" ")[0] ?? "";
|
|
234
|
+
const renderPermission = (perm) => {
|
|
235
|
+
if (!perm)
|
|
236
|
+
return perm;
|
|
237
|
+
const renderRules = (rules) => {
|
|
238
|
+
if (!rules)
|
|
239
|
+
return rules;
|
|
240
|
+
const out = {};
|
|
241
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
242
|
+
if (pattern.includes("<profile-test-command>") && testCommandToken === "") {
|
|
243
|
+
// Profile 未配置测试命令 → 删除占位规则,避免渲染成 "*" 覆盖兜底 deny
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
out[pattern.replaceAll("<profile-test-command>", testCommandToken)] = action;
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
};
|
|
250
|
+
const out = { ...perm };
|
|
251
|
+
if (perm.bash && typeof perm.bash === "object" && !Array.isArray(perm.bash)) {
|
|
252
|
+
out.bash = renderRules(perm.bash);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
};
|
|
413
256
|
for (const agent of loadAgents(agentsDir)) {
|
|
414
257
|
if (config.agent[agent.key])
|
|
415
258
|
continue;
|
|
@@ -418,14 +261,14 @@ export function createOpencodeCabbage(packageRoot) {
|
|
|
418
261
|
mode: agent.mode,
|
|
419
262
|
color: agent.color,
|
|
420
263
|
prompt: agent.prompt,
|
|
421
|
-
|
|
422
|
-
permission: agent.permission,
|
|
264
|
+
permission: renderPermission(agent.permission),
|
|
423
265
|
shell: {
|
|
424
|
-
env:
|
|
266
|
+
env: createAgentShellEnv(),
|
|
425
267
|
},
|
|
426
268
|
};
|
|
427
269
|
}
|
|
428
270
|
configureGoalTools(config);
|
|
271
|
+
configureLifecycleTools(config);
|
|
429
272
|
},
|
|
430
273
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
431
274
|
if (!output.messages.length)
|
|
@@ -433,13 +276,20 @@ export function createOpencodeCabbage(packageRoot) {
|
|
|
433
276
|
const firstUser = output.messages.find(m => m.info.role === "user");
|
|
434
277
|
if (!firstUser || !firstUser.parts.length)
|
|
435
278
|
return;
|
|
436
|
-
if (firstUser.parts.some(p => p.type === "text" && p.text.includes("EXTREMELY_IMPORTANT")))
|
|
279
|
+
if (firstUser.parts.some(p => p.type === "text" && (p.text.includes("EXTREMELY_IMPORTANT") || p.text.includes(CONTEXT_MARKER))))
|
|
437
280
|
return;
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
281
|
+
const sessionID = output.messages[0].info.sessionID;
|
|
282
|
+
const { goal, session } = await readGoal(goalClient, sessionID);
|
|
283
|
+
const contextBlock = await getContextBlock(projectDir);
|
|
284
|
+
const injection = buildFirstUserInjection({
|
|
285
|
+
hasGoal: goal?.status === "active",
|
|
286
|
+
isSubAgent: !!session?.parentID,
|
|
287
|
+
contextBlock,
|
|
288
|
+
bootstrap: getBootstrapContent(),
|
|
289
|
+
});
|
|
290
|
+
if (injection) {
|
|
291
|
+
firstUser.parts.unshift({ type: "text", text: injection });
|
|
292
|
+
}
|
|
443
293
|
},
|
|
444
294
|
async event({ event }) {
|
|
445
295
|
const evt = event;
|
|
@@ -453,10 +303,18 @@ export function createOpencodeCabbage(packageRoot) {
|
|
|
453
303
|
if (evt.type === "session.status" && evt.properties?.status?.type === "idle") {
|
|
454
304
|
const sessionID = evt.properties.sessionID;
|
|
455
305
|
if (sessionID) {
|
|
456
|
-
|
|
457
|
-
|
|
306
|
+
try {
|
|
307
|
+
const result = await goalClient.session.get({ sessionID });
|
|
308
|
+
if (result?.data?.parentID)
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
console.error("[cabbage] session lookup failed:", err);
|
|
458
313
|
return;
|
|
459
|
-
|
|
314
|
+
}
|
|
315
|
+
void queueContinuation(goalClient, sessionID, projectDir).catch(err => {
|
|
316
|
+
console.error("[cabbage] continuation failed:", err);
|
|
317
|
+
});
|
|
460
318
|
}
|
|
461
319
|
}
|
|
462
320
|
if (evt.type === "session.status" && evt.properties?.status?.type === "error") {
|
|
@@ -466,24 +324,36 @@ export function createOpencodeCabbage(packageRoot) {
|
|
|
466
324
|
const action = shouldEscalate(sessionID, phase);
|
|
467
325
|
const { goal } = await readGoal(goalClient, sessionID);
|
|
468
326
|
if (action === "retry" && goal) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
327
|
+
try {
|
|
328
|
+
await goalClient.session.promptAsync({
|
|
329
|
+
sessionID,
|
|
330
|
+
parts: [{
|
|
331
|
+
type: "text",
|
|
332
|
+
text: `[auto-retry] Previous attempt failed. Try a different approach.\n\n${formatGoal(goal)}`,
|
|
333
|
+
synthetic: true,
|
|
334
|
+
}],
|
|
335
|
+
...(goal.sessionProfile ? { agent: goal.sessionProfile.agent, model: goal.sessionProfile.model } : {}),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
console.error("[cabbage] auto-retry prompt failed:", err);
|
|
340
|
+
}
|
|
477
341
|
}
|
|
478
342
|
else if (action === "skip" && goal) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
343
|
+
try {
|
|
344
|
+
await goalClient.session.promptAsync({
|
|
345
|
+
sessionID,
|
|
346
|
+
parts: [{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: `[skip] Skipping failed step. Continue with remaining work.\n\n${formatGoal(goal)}`,
|
|
349
|
+
synthetic: true,
|
|
350
|
+
}],
|
|
351
|
+
...(goal.sessionProfile ? { agent: goal.sessionProfile.agent, model: goal.sessionProfile.model } : {}),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
console.error("[cabbage] auto-skip prompt failed:", err);
|
|
356
|
+
}
|
|
487
357
|
}
|
|
488
358
|
}
|
|
489
359
|
}
|
|
@@ -494,6 +364,14 @@ export function createOpencodeCabbage(packageRoot) {
|
|
|
494
364
|
const { goal, session } = await readGoal(goalClient, sessionID);
|
|
495
365
|
if (goal) {
|
|
496
366
|
goal.continuationCount = 0;
|
|
367
|
+
// 刷新用户最后使用的 agent/模型快照(用户在 TUI 切换 agent/模型后发消息)
|
|
368
|
+
const info = evt.properties.info;
|
|
369
|
+
if (info?.agent) {
|
|
370
|
+
goal.sessionProfile = {
|
|
371
|
+
agent: info.agent,
|
|
372
|
+
model: info.model?.providerID ? { providerID: info.model.providerID, modelID: info.model.modelID } : goal.sessionProfile?.model,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
497
375
|
await writeGoal(goalClient, sessionID, goal, session ?? undefined);
|
|
498
376
|
}
|
|
499
377
|
}
|
|
@@ -503,7 +381,7 @@ export function createOpencodeCabbage(packageRoot) {
|
|
|
503
381
|
if (sessionID) {
|
|
504
382
|
const { goal } = await readGoal(goalClient, sessionID);
|
|
505
383
|
if (goal?.status === "complete") {
|
|
506
|
-
await
|
|
384
|
+
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: "completed" });
|
|
507
385
|
}
|
|
508
386
|
}
|
|
509
387
|
}
|