@fkqfkq123/opencode-autopilot 0.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 +462 -0
- package/README.zh-CN.md +464 -0
- package/dist/packages/adapters/opencode/src/opencode-session-client.d.ts +188 -0
- package/dist/packages/adapters/opencode/src/opencode-session-client.js +382 -0
- package/dist/packages/core/src/artifacts/artifact-evaluator.d.ts +17 -0
- package/dist/packages/core/src/artifacts/artifact-evaluator.js +1 -0
- package/dist/packages/core/src/artifacts/artifact.d.ts +7 -0
- package/dist/packages/core/src/artifacts/artifact.js +1 -0
- package/dist/packages/core/src/human-actions/human-action-record.d.ts +10 -0
- package/dist/packages/core/src/human-actions/human-action-record.js +1 -0
- package/dist/packages/core/src/human-actions/human-action.d.ts +13 -0
- package/dist/packages/core/src/human-actions/human-action.js +1 -0
- package/dist/packages/core/src/human-actions/question.d.ts +8 -0
- package/dist/packages/core/src/human-actions/question.js +1 -0
- package/dist/packages/core/src/state/phase.d.ts +2 -0
- package/dist/packages/core/src/state/phase.js +1 -0
- package/dist/packages/core/src/state/workflow-runtime-state.d.ts +14 -0
- package/dist/packages/core/src/state/workflow-runtime-state.js +1 -0
- package/dist/packages/core/src/state/workflow-state.d.ts +13 -0
- package/dist/packages/core/src/state/workflow-state.js +1 -0
- package/dist/packages/core/src/transitions/default-phase-transition.d.ts +5 -0
- package/dist/packages/core/src/transitions/default-phase-transition.js +195 -0
- package/dist/packages/core/src/transitions/phase-transition.d.ts +22 -0
- package/dist/packages/core/src/transitions/phase-transition.js +1 -0
- package/dist/packages/core/src/transitions/transition-action.d.ts +20 -0
- package/dist/packages/core/src/transitions/transition-action.js +1 -0
- package/dist/packages/runtime/src/artifacts/file-system-artifact-evaluator.d.ts +36 -0
- package/dist/packages/runtime/src/artifacts/file-system-artifact-evaluator.js +1213 -0
- package/dist/packages/runtime/src/attach/attach-service.d.ts +15 -0
- package/dist/packages/runtime/src/attach/attach-service.js +31 -0
- package/dist/packages/runtime/src/bootstrap/create-harness.d.ts +33 -0
- package/dist/packages/runtime/src/bootstrap/create-harness.js +79 -0
- package/dist/packages/runtime/src/bootstrap/initialize-workflow.d.ts +8 -0
- package/dist/packages/runtime/src/bootstrap/initialize-workflow.js +33 -0
- package/dist/packages/runtime/src/commands/create-opencode-workflow-commands.d.ts +12 -0
- package/dist/packages/runtime/src/commands/create-opencode-workflow-commands.js +24 -0
- package/dist/packages/runtime/src/commands/default-workflow-command-runner.d.ts +4 -0
- package/dist/packages/runtime/src/commands/default-workflow-command-runner.js +343 -0
- package/dist/packages/runtime/src/commands/opencode-plugin-command-adapter.d.ts +20 -0
- package/dist/packages/runtime/src/commands/opencode-plugin-command-adapter.js +22 -0
- package/dist/packages/runtime/src/commands/workflow-command-runner.d.ts +19 -0
- package/dist/packages/runtime/src/commands/workflow-command-runner.js +1 -0
- package/dist/packages/runtime/src/commands/workflow-open-request.d.ts +10 -0
- package/dist/packages/runtime/src/commands/workflow-open-request.js +220 -0
- package/dist/packages/runtime/src/config/skill-registry.d.ts +15 -0
- package/dist/packages/runtime/src/config/skill-registry.js +108 -0
- package/dist/packages/runtime/src/config/workflow-config.d.ts +17 -0
- package/dist/packages/runtime/src/config/workflow-config.js +51 -0
- package/dist/packages/runtime/src/diagnostics/workflow-diagnostics-format.d.ts +4 -0
- package/dist/packages/runtime/src/diagnostics/workflow-diagnostics-format.js +70 -0
- package/dist/packages/runtime/src/diagnostics/workflow-doctor.d.ts +23 -0
- package/dist/packages/runtime/src/diagnostics/workflow-doctor.js +120 -0
- package/dist/packages/runtime/src/engine/default-workflow-engine.d.ts +9 -0
- package/dist/packages/runtime/src/engine/default-workflow-engine.js +337 -0
- package/dist/packages/runtime/src/engine/workflow-engine.d.ts +28 -0
- package/dist/packages/runtime/src/engine/workflow-engine.js +1 -0
- package/dist/packages/runtime/src/events/file-system-workflow-event-store.d.ts +8 -0
- package/dist/packages/runtime/src/events/file-system-workflow-event-store.js +28 -0
- package/dist/packages/runtime/src/events/workflow-event-store.d.ts +10 -0
- package/dist/packages/runtime/src/events/workflow-event-store.js +1 -0
- package/dist/packages/runtime/src/index.d.ts +4 -0
- package/dist/packages/runtime/src/index.js +4 -0
- package/dist/packages/runtime/src/install/workflow-installer.d.ts +15 -0
- package/dist/packages/runtime/src/install/workflow-installer.js +111 -0
- package/dist/packages/runtime/src/plugin/workflow-plugin-entry.d.ts +167 -0
- package/dist/packages/runtime/src/plugin/workflow-plugin-entry.js +340 -0
- package/dist/packages/runtime/src/presentation/human-action-renderer.d.ts +13 -0
- package/dist/packages/runtime/src/presentation/human-action-renderer.js +161 -0
- package/dist/packages/runtime/src/presentation/watch-renderer.d.ts +12 -0
- package/dist/packages/runtime/src/presentation/watch-renderer.js +17 -0
- package/dist/packages/runtime/src/recovery/basic-recovery-classifier.d.ts +4 -0
- package/dist/packages/runtime/src/recovery/basic-recovery-classifier.js +12 -0
- package/dist/packages/runtime/src/recovery/recovery-classifier.d.ts +4 -0
- package/dist/packages/runtime/src/recovery/recovery-classifier.js +1 -0
- package/dist/packages/runtime/src/scheduling/immediate-tick-scheduler.d.ts +9 -0
- package/dist/packages/runtime/src/scheduling/immediate-tick-scheduler.js +28 -0
- package/dist/packages/runtime/src/scheduling/tick-scheduler.d.ts +3 -0
- package/dist/packages/runtime/src/scheduling/tick-scheduler.js +1 -0
- package/dist/packages/runtime/src/sessions/file-system-session-coordinator.d.ts +19 -0
- package/dist/packages/runtime/src/sessions/file-system-session-coordinator.js +132 -0
- package/dist/packages/runtime/src/sessions/session-activity-monitor.d.ts +22 -0
- package/dist/packages/runtime/src/sessions/session-activity-monitor.js +112 -0
- package/dist/packages/runtime/src/sessions/session-coordinator.d.ts +24 -0
- package/dist/packages/runtime/src/sessions/session-coordinator.js +1 -0
- package/dist/packages/runtime/src/shared/json-file.d.ts +2 -0
- package/dist/packages/runtime/src/shared/json-file.js +19 -0
- package/dist/packages/runtime/src/state/file-system-human-action-store.d.ts +15 -0
- package/dist/packages/runtime/src/state/file-system-human-action-store.js +69 -0
- package/dist/packages/runtime/src/state/file-system-workflow-state-store.d.ts +15 -0
- package/dist/packages/runtime/src/state/file-system-workflow-state-store.js +59 -0
- package/dist/packages/runtime/src/state/human-action-service.d.ts +21 -0
- package/dist/packages/runtime/src/state/human-action-service.js +80 -0
- package/dist/packages/runtime/src/state/human-action-store.d.ts +9 -0
- package/dist/packages/runtime/src/state/human-action-store.js +1 -0
- package/dist/packages/runtime/src/state/workflow-state-store.d.ts +11 -0
- package/dist/packages/runtime/src/state/workflow-state-store.js +1 -0
- package/dist/packages/runtime/src/subtasks/noop-subtask-tracker.d.ts +4 -0
- package/dist/packages/runtime/src/subtasks/noop-subtask-tracker.js +5 -0
- package/dist/packages/runtime/src/subtasks/subtask-tracker.d.ts +3 -0
- package/dist/packages/runtime/src/subtasks/subtask-tracker.js +1 -0
- package/dist/packages/runtime/src/workspace/workflow-workspace.d.ts +31 -0
- package/dist/packages/runtime/src/workspace/workflow-workspace.js +43 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +1 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +175 -0
- package/package.json +56 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { loadResolvedSkillContents, resolveSkillPaths } from "../config/skill-registry";
|
|
3
|
+
export class DefaultWorkflowEngine {
|
|
4
|
+
deps;
|
|
5
|
+
constructor(deps) {
|
|
6
|
+
this.deps = deps;
|
|
7
|
+
}
|
|
8
|
+
isArtifactPhase(phase) {
|
|
9
|
+
return phase === "spec_refinement"
|
|
10
|
+
|| phase === "plan"
|
|
11
|
+
|| phase === "develop"
|
|
12
|
+
|| phase === "review"
|
|
13
|
+
|| phase === "test";
|
|
14
|
+
}
|
|
15
|
+
buildRefinementDispatchSummary(args) {
|
|
16
|
+
return `attempt=${args.attempt}; missing=${args.missing.length}; openQuestions=${args.questionCount}`;
|
|
17
|
+
}
|
|
18
|
+
async buildDispatchPrompt(workflowId, phase, reason) {
|
|
19
|
+
if (!this.isArtifactPhase(phase)) {
|
|
20
|
+
return reason;
|
|
21
|
+
}
|
|
22
|
+
const workflow = await this.deps.stateStore.getWorkflow(workflowId);
|
|
23
|
+
const artifact = workflow ? await this.deps.artifactEvaluator.evaluate(workflow) : null;
|
|
24
|
+
let currentContent = "";
|
|
25
|
+
try {
|
|
26
|
+
currentContent = await readFile(this.deps.workspace.phaseArtifactFile(workflowId, phase), "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
if (!message.includes("ENOENT")) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const phaseGoalByType = {
|
|
35
|
+
spec_refinement: "Produce a refinement artifact that can reach READY_FOR_PLAN once true ambiguities are resolved.",
|
|
36
|
+
plan: "Produce a concrete implementation plan aligned to the refinement artifact and keep the plan document complete.",
|
|
37
|
+
develop: "Implement the approved plan, update project code, and write a completed develop report artifact.",
|
|
38
|
+
review: "Perform review against implementation and write a review report with PASS/FAIL conclusion and severity summary.",
|
|
39
|
+
test: "Run verification/testing and write a test report with PASS/FAIL status and clear evidence.",
|
|
40
|
+
};
|
|
41
|
+
const completionPolicyByPhase = {
|
|
42
|
+
spec_refinement: "Set 准入结论 to READY_FOR_PLAN when ambiguity is truly resolved.",
|
|
43
|
+
plan: "Keep all required sections complete and ensure plan is approvable.",
|
|
44
|
+
develop: "Set ## 状态 to COMPLETED/通过/完成 only after implementation and self-check are done.",
|
|
45
|
+
review: "Set ## 状态 and ## 结论 with explicit pass/fail semantics and include issue severity summary.",
|
|
46
|
+
test: "Set ## 状态 and ## 结论 with explicit pass/fail semantics and include regression/coverage evidence.",
|
|
47
|
+
};
|
|
48
|
+
const lines = [
|
|
49
|
+
`[PHASE] ${phase}`,
|
|
50
|
+
`[GOAL] ${phaseGoalByType[phase]}`,
|
|
51
|
+
`[REASON] ${reason}`,
|
|
52
|
+
];
|
|
53
|
+
if (phase === "plan") {
|
|
54
|
+
try {
|
|
55
|
+
const refinementContent = await readFile(this.deps.workspace.phaseArtifactFile(workflowId, "spec_refinement"), "utf8");
|
|
56
|
+
lines.push("[SOURCE_REFINEMENT_ARTIFACT]");
|
|
57
|
+
lines.push(refinementContent);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
if (!message.includes("ENOENT")) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (phase === "develop") {
|
|
67
|
+
try {
|
|
68
|
+
const planContent = await readFile(this.deps.workspace.phaseArtifactFile(workflowId, "plan"), "utf8");
|
|
69
|
+
lines.push("[SOURCE_PLAN_ARTIFACT]");
|
|
70
|
+
lines.push(planContent);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
if (!message.includes("ENOENT")) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (phase === "review") {
|
|
80
|
+
try {
|
|
81
|
+
const developContent = await readFile(this.deps.workspace.phaseArtifactFile(workflowId, "develop"), "utf8");
|
|
82
|
+
lines.push("[SOURCE_DEVELOP_ARTIFACT]");
|
|
83
|
+
lines.push(developContent);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
if (!message.includes("ENOENT")) {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (phase === "test") {
|
|
93
|
+
try {
|
|
94
|
+
const reviewContent = await readFile(this.deps.workspace.phaseArtifactFile(workflowId, "review"), "utf8");
|
|
95
|
+
lines.push("[SOURCE_REVIEW_ARTIFACT]");
|
|
96
|
+
lines.push(reviewContent);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
if (!message.includes("ENOENT")) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (artifact?.missing && artifact.missing.length > 0) {
|
|
106
|
+
lines.push("[MISSING]");
|
|
107
|
+
lines.push(...artifact.missing.map((item) => `- ${item}`));
|
|
108
|
+
}
|
|
109
|
+
if (artifact?.questions && artifact.questions.length > 0) {
|
|
110
|
+
lines.push("[OPEN_QUESTIONS]");
|
|
111
|
+
lines.push(...artifact.questions.map((question) => `- (${question.id}) [${question.priority}] ${question.text}`));
|
|
112
|
+
}
|
|
113
|
+
lines.push("[POLICY] Autofill what can be safely inferred. Preserve existing content. Ask humans only for genuine ambiguity. Keep section headings unchanged.");
|
|
114
|
+
if (phase === "spec_refinement") {
|
|
115
|
+
lines.push("[AI_INTAKE_POLICY] Interpret user natural language directly. Extract requirement intent, infer referenced document locations from user wording, and read project documents with your tools before updating artifact sections. Use [DOC_CANDIDATES] as recall-only hints; make final relevance decisions semantically. Do not require user to provide structured JSON.");
|
|
116
|
+
}
|
|
117
|
+
if (phase === "plan") {
|
|
118
|
+
lines.push("[PLAN_POLICY] Build a concrete implementation plan from the refinement artifact. Replace placeholder text with repository-specific scope, file impact, implementation steps, risk analysis, approval-ready detail, and explicit regression considerations for existing functionality that may be affected. Keep section headings unchanged.");
|
|
119
|
+
}
|
|
120
|
+
if (phase === "develop") {
|
|
121
|
+
lines.push("[DEVELOP_POLICY] Execute against the approved plan artifact. Update code first, then rewrite the develop artifact with actual changed files, supporting changes, self-check evidence, and explicit regression checks for impacted existing behavior. Set ## 状态 to COMPLETED only when implementation and validation are truly done.");
|
|
122
|
+
}
|
|
123
|
+
if (phase === "review") {
|
|
124
|
+
lines.push("[REVIEW_POLICY] Review the implementation against the develop artifact and plan intent. Record concrete findings, severity summary, regression risk to existing functionality, and set explicit pass/fail conclusion in the review artifact. Keep section headings unchanged.");
|
|
125
|
+
}
|
|
126
|
+
if (phase === "test") {
|
|
127
|
+
lines.push("[TEST_POLICY] Validate the implementation and review findings. Update the test artifact with executed checks, failures, regression evidence for previously working features, coverage summary, and set explicit pass/fail conclusion. Keep section headings unchanged.");
|
|
128
|
+
}
|
|
129
|
+
if ((phase === "spec_refinement" || phase === "plan" || phase === "develop" || phase === "review" || phase === "test") && this.deps.resolvedConfig?.phases?.[phase]?.requiredSkills?.length) {
|
|
130
|
+
const requiredSkills = this.deps.resolvedConfig.phases[phase]?.requiredSkills ?? [];
|
|
131
|
+
const resolvedSkills = this.deps.skillRegistry
|
|
132
|
+
? resolveSkillPaths(this.deps.skillRegistry, requiredSkills)
|
|
133
|
+
: [];
|
|
134
|
+
const loadedSkills = this.deps.skillRegistry
|
|
135
|
+
? await loadResolvedSkillContents(this.deps.skillRegistry, requiredSkills)
|
|
136
|
+
: [];
|
|
137
|
+
lines.push("[REQUIRED_SKILLS]");
|
|
138
|
+
for (const skillName of requiredSkills) {
|
|
139
|
+
const resolved = resolvedSkills.find((entry) => entry.name === skillName);
|
|
140
|
+
lines.push(resolved ? `- ${skillName} :: ${resolved.path}` : `- ${skillName}`);
|
|
141
|
+
}
|
|
142
|
+
const missingSkills = requiredSkills.filter((skillName) => !resolvedSkills.some((entry) => entry.name === skillName));
|
|
143
|
+
if (missingSkills.length > 0) {
|
|
144
|
+
lines.push("[MISSING_SKILLS]");
|
|
145
|
+
lines.push(...missingSkills.map((skillName) => `- ${skillName}`));
|
|
146
|
+
}
|
|
147
|
+
if (loadedSkills.length > 0) {
|
|
148
|
+
lines.push("[SKILL_CONTENT]");
|
|
149
|
+
for (const skill of loadedSkills) {
|
|
150
|
+
lines.push(`[SKILL ${skill.name}]`);
|
|
151
|
+
lines.push(skill.content);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (this.deps.resolvedConfig?.warnings?.length) {
|
|
156
|
+
lines.push("[CONFIG_WARNINGS]");
|
|
157
|
+
lines.push(...this.deps.resolvedConfig.warnings.map((warning) => `- ${warning}`));
|
|
158
|
+
}
|
|
159
|
+
lines.push(`[COMPLETION_POLICY] ${completionPolicyByPhase[phase]}`);
|
|
160
|
+
lines.push(`[ARTIFACT_PATH] ${this.deps.workspace.phaseArtifactFile(workflowId, phase)}`);
|
|
161
|
+
if (currentContent) {
|
|
162
|
+
lines.push("[CURRENT_ARTIFACT]");
|
|
163
|
+
lines.push(currentContent);
|
|
164
|
+
}
|
|
165
|
+
return lines.join("\n\n");
|
|
166
|
+
}
|
|
167
|
+
async tick(workflowId) {
|
|
168
|
+
const workflow = await this.deps.stateStore.getWorkflow(workflowId);
|
|
169
|
+
if (!workflow) {
|
|
170
|
+
throw new Error(`Workflow not found: ${workflowId}`);
|
|
171
|
+
}
|
|
172
|
+
const runtime = await this.deps.stateStore.getRuntime(workflowId);
|
|
173
|
+
if (!runtime) {
|
|
174
|
+
throw new Error(`Runtime state not found: ${workflowId}`);
|
|
175
|
+
}
|
|
176
|
+
const artifact = await this.deps.artifactEvaluator.evaluate(workflow);
|
|
177
|
+
const currentHumanAction = await this.deps.humanActionStore.getCurrent(workflowId);
|
|
178
|
+
const session = await this.deps.sessionCoordinator.getRelevantSession(workflowId);
|
|
179
|
+
const hasRunningSubtasks = await this.deps.subtaskTracker.hasRunningSubtasks(workflowId);
|
|
180
|
+
const action = await this.deps.phaseTransition.decide({
|
|
181
|
+
workflow,
|
|
182
|
+
runtime,
|
|
183
|
+
artifact,
|
|
184
|
+
currentHumanAction,
|
|
185
|
+
session,
|
|
186
|
+
hasRunningSubtasks,
|
|
187
|
+
});
|
|
188
|
+
switch (action.type) {
|
|
189
|
+
case "wait_human": {
|
|
190
|
+
if (!currentHumanAction || currentHumanAction.status === "consumed") {
|
|
191
|
+
const record = await this.deps.humanActionStore.create(action.action);
|
|
192
|
+
await this.deps.humanActionStore.markPresented(record.id);
|
|
193
|
+
await this.deps.stateStore.updateWorkflow(workflowId, {
|
|
194
|
+
status: "waiting_human",
|
|
195
|
+
});
|
|
196
|
+
const waitRuntimePatch = {
|
|
197
|
+
waitingHumanActionId: record.id,
|
|
198
|
+
...(workflow.phase === "spec_refinement" && (runtime.refinementAttempts ?? 0) > 0
|
|
199
|
+
? { refinementEscalationReason: "Autonomous refinement retry budget exhausted" }
|
|
200
|
+
: {}),
|
|
201
|
+
};
|
|
202
|
+
await this.deps.stateStore.updateRuntime(workflowId, waitRuntimePatch);
|
|
203
|
+
await this.deps.eventStore.append({
|
|
204
|
+
workflowId,
|
|
205
|
+
type: "human_action.required",
|
|
206
|
+
at: new Date().toISOString(),
|
|
207
|
+
payload: {
|
|
208
|
+
humanActionId: record.id,
|
|
209
|
+
actionType: action.action.type,
|
|
210
|
+
phase: action.action.phase,
|
|
211
|
+
reason: action.action.reason,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
case "advance_phase": {
|
|
218
|
+
const iterationPatch = workflow.phase === "review"
|
|
219
|
+
&& action.nextPhase === "develop"
|
|
220
|
+
? { iteration: workflow.iteration + 1 }
|
|
221
|
+
: {};
|
|
222
|
+
if (workflow.phase === "test" && action.nextPhase === "done") {
|
|
223
|
+
const finalArtifact = await this.deps.artifactEvaluator.evaluate(workflow);
|
|
224
|
+
if (!(finalArtifact.valid && finalArtifact.missing.length === 0 && finalArtifact.reportStatus === "pass")) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (currentHumanAction && currentHumanAction.status !== "consumed") {
|
|
229
|
+
await this.deps.humanActionStore.markConsumed(currentHumanAction.id);
|
|
230
|
+
await this.deps.stateStore.updateRuntime(workflowId, {
|
|
231
|
+
waitingHumanActionId: null,
|
|
232
|
+
...(workflow.phase === "spec_refinement"
|
|
233
|
+
? {
|
|
234
|
+
refinementAttempts: 0,
|
|
235
|
+
refinementLastDispatchSummary: null,
|
|
236
|
+
refinementEscalationReason: null,
|
|
237
|
+
}
|
|
238
|
+
: {}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
await this.deps.sessionCoordinator.archiveIrrelevantSessions(workflowId, action.nextPhase);
|
|
242
|
+
await this.deps.artifactEvaluator.prepareForPhase?.(workflowId, action.nextPhase, workflow.phase);
|
|
243
|
+
await this.deps.stateStore.updateWorkflow(workflowId, {
|
|
244
|
+
phase: action.nextPhase,
|
|
245
|
+
status: action.nextPhase === "done" ? "completed" : "pending",
|
|
246
|
+
phaseEnteredAt: new Date().toISOString(),
|
|
247
|
+
activeSessionId: null,
|
|
248
|
+
blockReason: null,
|
|
249
|
+
...iterationPatch,
|
|
250
|
+
});
|
|
251
|
+
await this.deps.eventStore.append({
|
|
252
|
+
workflowId,
|
|
253
|
+
type: "phase.changed",
|
|
254
|
+
at: new Date().toISOString(),
|
|
255
|
+
payload: {
|
|
256
|
+
from: workflow.phase,
|
|
257
|
+
to: action.nextPhase,
|
|
258
|
+
iteration: workflow.phase === "review" && action.nextPhase === "develop"
|
|
259
|
+
? workflow.iteration + 1
|
|
260
|
+
: workflow.iteration,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
if (action.nextPhase !== "done") {
|
|
264
|
+
await this.deps.tickScheduler.requestTick(workflowId, "phase advanced");
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
case "dispatch": {
|
|
269
|
+
const sessionId = await this.deps.sessionCoordinator.ensureSession(workflowId, action.phase, action.phase === "develop" || action.phase === "review" || action.phase === "test"
|
|
270
|
+
? runtime.preferredForegroundSessionId ?? null
|
|
271
|
+
: null);
|
|
272
|
+
const prompt = await this.buildDispatchPrompt(workflowId, action.phase, action.reason);
|
|
273
|
+
const nextAttempt = action.phase === "spec_refinement"
|
|
274
|
+
? (runtime.refinementAttempts ?? 0) + 1
|
|
275
|
+
: 0;
|
|
276
|
+
const runtimePatch = {
|
|
277
|
+
lastContinuationAt: new Date().toISOString(),
|
|
278
|
+
...(action.phase === "spec_refinement"
|
|
279
|
+
? {
|
|
280
|
+
refinementAttempts: nextAttempt,
|
|
281
|
+
refinementLastDispatchSummary: this.buildRefinementDispatchSummary({
|
|
282
|
+
missing: artifact.missing,
|
|
283
|
+
questionCount: artifact.questions?.length ?? 0,
|
|
284
|
+
attempt: nextAttempt,
|
|
285
|
+
}),
|
|
286
|
+
refinementEscalationReason: null,
|
|
287
|
+
}
|
|
288
|
+
: {}),
|
|
289
|
+
};
|
|
290
|
+
await this.deps.stateStore.updateWorkflow(workflowId, {
|
|
291
|
+
activeSessionId: sessionId,
|
|
292
|
+
status: "in_progress",
|
|
293
|
+
});
|
|
294
|
+
await this.deps.sessionCoordinator.inject(workflowId, sessionId, prompt);
|
|
295
|
+
await this.deps.stateStore.updateRuntime(workflowId, runtimePatch);
|
|
296
|
+
await this.deps.eventStore.append({
|
|
297
|
+
workflowId,
|
|
298
|
+
type: "session.dispatched",
|
|
299
|
+
at: new Date().toISOString(),
|
|
300
|
+
payload: {
|
|
301
|
+
sessionId,
|
|
302
|
+
phase: action.phase,
|
|
303
|
+
reason: prompt,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
case "recover": {
|
|
309
|
+
const disposition = this.deps.recoveryClassifier.classify(action.reason);
|
|
310
|
+
await this.deps.stateStore.updateRuntime(workflowId, {
|
|
311
|
+
recoveryState: disposition === "terminal" ? "idle" : "recovering",
|
|
312
|
+
consecutiveFailures: runtime.consecutiveFailures + 1,
|
|
313
|
+
});
|
|
314
|
+
if (disposition === "retryable") {
|
|
315
|
+
await this.deps.tickScheduler.requestTick(workflowId, "retry recovery");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
await this.deps.stateStore.updateWorkflow(workflowId, {
|
|
319
|
+
phase: "blocked",
|
|
320
|
+
status: "blocked",
|
|
321
|
+
blockReason: action.reason,
|
|
322
|
+
});
|
|
323
|
+
await this.deps.eventStore.append({
|
|
324
|
+
workflowId,
|
|
325
|
+
type: "workflow.blocked",
|
|
326
|
+
at: new Date().toISOString(),
|
|
327
|
+
payload: {
|
|
328
|
+
reason: action.reason,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
case "stop":
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ArtifactEvaluator } from "../../../core/src/artifacts/artifact-evaluator";
|
|
2
|
+
import type { PhaseTransition } from "../../../core/src/transitions/phase-transition";
|
|
3
|
+
import type { RecoveryClassifier } from "../recovery/recovery-classifier";
|
|
4
|
+
import type { TickScheduler } from "../scheduling/tick-scheduler";
|
|
5
|
+
import type { SessionCoordinator } from "../sessions/session-coordinator";
|
|
6
|
+
import type { HumanActionStore } from "../state/human-action-store";
|
|
7
|
+
import type { WorkflowStateStore } from "../state/workflow-state-store";
|
|
8
|
+
import type { SubtaskTracker } from "../subtasks/subtask-tracker";
|
|
9
|
+
import type { WorkflowEventStore } from "../events/workflow-event-store";
|
|
10
|
+
import type { WorkflowWorkspace } from "../workspace/workflow-workspace";
|
|
11
|
+
import type { ResolvedWorkflowConfig } from "../config/workflow-config";
|
|
12
|
+
export interface WorkflowEngine {
|
|
13
|
+
tick(workflowId: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface WorkflowEngineDeps {
|
|
16
|
+
stateStore: WorkflowStateStore;
|
|
17
|
+
humanActionStore: HumanActionStore;
|
|
18
|
+
artifactEvaluator: ArtifactEvaluator;
|
|
19
|
+
phaseTransition: PhaseTransition;
|
|
20
|
+
sessionCoordinator: SessionCoordinator;
|
|
21
|
+
recoveryClassifier: RecoveryClassifier;
|
|
22
|
+
subtaskTracker: SubtaskTracker;
|
|
23
|
+
tickScheduler: TickScheduler;
|
|
24
|
+
eventStore: WorkflowEventStore;
|
|
25
|
+
workspace: WorkflowWorkspace;
|
|
26
|
+
resolvedConfig?: ResolvedWorkflowConfig;
|
|
27
|
+
skillRegistry?: Map<string, string>;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WorkflowWorkspace } from "../workspace/workflow-workspace";
|
|
2
|
+
import type { WorkflowEventRecord, WorkflowEventStore } from "./workflow-event-store";
|
|
3
|
+
export declare class FileSystemWorkflowEventStore implements WorkflowEventStore {
|
|
4
|
+
private readonly workspace;
|
|
5
|
+
constructor(workspace: WorkflowWorkspace);
|
|
6
|
+
append(event: WorkflowEventRecord): Promise<void>;
|
|
7
|
+
list(workflowId: string): Promise<WorkflowEventRecord[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { appendFile, readFile } from "node:fs/promises";
|
|
2
|
+
export class FileSystemWorkflowEventStore {
|
|
3
|
+
workspace;
|
|
4
|
+
constructor(workspace) {
|
|
5
|
+
this.workspace = workspace;
|
|
6
|
+
}
|
|
7
|
+
async append(event) {
|
|
8
|
+
const line = `${JSON.stringify(event)}\n`;
|
|
9
|
+
await appendFile(this.workspace.eventsFile(event.workflowId), line, "utf8");
|
|
10
|
+
}
|
|
11
|
+
async list(workflowId) {
|
|
12
|
+
try {
|
|
13
|
+
const content = await readFile(this.workspace.eventsFile(workflowId), "utf8");
|
|
14
|
+
return content
|
|
15
|
+
.split("\n")
|
|
16
|
+
.map((line) => line.trim())
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.map((line) => JSON.parse(line));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
if (message.includes("ENOENT")) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface WorkflowEventRecord {
|
|
2
|
+
workflowId: string;
|
|
3
|
+
type: string;
|
|
4
|
+
at: string;
|
|
5
|
+
payload?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface WorkflowEventStore {
|
|
8
|
+
append(event: WorkflowEventRecord): Promise<void>;
|
|
9
|
+
list(workflowId: string): Promise<WorkflowEventRecord[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createWorkflowPluginEntry, workflowPlugin } from "./plugin/workflow-plugin-entry";
|
|
2
|
+
export { createOpencodeWorkflowCommands } from "./commands/create-opencode-workflow-commands";
|
|
3
|
+
export { DefaultWorkflowPluginCommandAdapter } from "./commands/opencode-plugin-command-adapter";
|
|
4
|
+
export { DefaultWorkflowCommandRunner } from "./commands/default-workflow-command-runner";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createWorkflowPluginEntry, workflowPlugin } from "./plugin/workflow-plugin-entry";
|
|
2
|
+
export { createOpencodeWorkflowCommands } from "./commands/create-opencode-workflow-commands";
|
|
3
|
+
export { DefaultWorkflowPluginCommandAdapter } from "./commands/opencode-plugin-command-adapter";
|
|
4
|
+
export { DefaultWorkflowCommandRunner } from "./commands/default-workflow-command-runner";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type WorkflowInstallResult = {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
projectWorkflowConfigFile: string;
|
|
4
|
+
opencodeConfigFile: string;
|
|
5
|
+
warnings: string[];
|
|
6
|
+
pluginEntry: string;
|
|
7
|
+
};
|
|
8
|
+
export type WorkflowInstallOptions = {
|
|
9
|
+
pluginEntryFile?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function runWorkflowInstall(args: {
|
|
12
|
+
cwd: string;
|
|
13
|
+
homeDir: string;
|
|
14
|
+
options?: WorkflowInstallOptions;
|
|
15
|
+
}): Promise<WorkflowInstallResult>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { writeJsonFile } from "../shared/json-file";
|
|
5
|
+
const DEFAULT_WORKFLOW_CONFIG = {
|
|
6
|
+
skillRoots: ["~/.claude/skills", "~/.config/opencode/skills"],
|
|
7
|
+
phases: {
|
|
8
|
+
develop: { requiredSkills: [] },
|
|
9
|
+
test: { requiredSkills: [] },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
async function fileExists(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
await access(filePath);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function stripJsonComments(input) {
|
|
22
|
+
return input
|
|
23
|
+
.replace(/^\s*\/\/.*$/gm, "")
|
|
24
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
25
|
+
}
|
|
26
|
+
async function resolveOpencodeConfigFile(opencodeConfigDir) {
|
|
27
|
+
const jsonFile = join(opencodeConfigDir, "opencode.json");
|
|
28
|
+
const jsoncFile = join(opencodeConfigDir, "opencode.jsonc");
|
|
29
|
+
const hasJson = await fileExists(jsonFile);
|
|
30
|
+
const hasJsonc = await fileExists(jsoncFile);
|
|
31
|
+
const warnings = [];
|
|
32
|
+
if (hasJson && hasJsonc) {
|
|
33
|
+
warnings.push(`Both opencode.json and opencode.jsonc exist under ${opencodeConfigDir}; installer will prefer opencode.json`);
|
|
34
|
+
}
|
|
35
|
+
if (hasJson) {
|
|
36
|
+
return { filePath: jsonFile, warnings };
|
|
37
|
+
}
|
|
38
|
+
if (hasJsonc) {
|
|
39
|
+
warnings.push(`Existing opencode.jsonc detected; installer will normalize output into opencode.json`);
|
|
40
|
+
return { filePath: jsoncFile, warnings };
|
|
41
|
+
}
|
|
42
|
+
return { filePath: jsonFile, warnings };
|
|
43
|
+
}
|
|
44
|
+
export async function runWorkflowInstall(args) {
|
|
45
|
+
const repoRoot = resolve(args.cwd);
|
|
46
|
+
const harnessDir = join(repoRoot, ".workflow-harness");
|
|
47
|
+
const projectWorkflowConfigFile = join(harnessDir, "workflow.json");
|
|
48
|
+
const opencodeConfigDir = join(args.homeDir, ".config", "opencode");
|
|
49
|
+
const pluginEntryFile = args.options?.pluginEntryFile ?? "dist/plugin.js";
|
|
50
|
+
const pluginEntry = `file://${join(repoRoot, pluginEntryFile)}`;
|
|
51
|
+
const configResolution = await resolveOpencodeConfigFile(opencodeConfigDir);
|
|
52
|
+
const opencodeConfigFile = join(opencodeConfigDir, "opencode.json");
|
|
53
|
+
const warnings = [...configResolution.warnings];
|
|
54
|
+
await mkdir(harnessDir, { recursive: true });
|
|
55
|
+
if (!(await fileExists(projectWorkflowConfigFile))) {
|
|
56
|
+
await writeJsonFile(projectWorkflowConfigFile, DEFAULT_WORKFLOW_CONFIG);
|
|
57
|
+
}
|
|
58
|
+
await mkdir(opencodeConfigDir, { recursive: true });
|
|
59
|
+
if (!(await fileExists(configResolution.filePath))) {
|
|
60
|
+
await writeJsonFile(opencodeConfigFile, {
|
|
61
|
+
plugin: [pluginEntry],
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
projectWorkflowConfigFile,
|
|
66
|
+
opencodeConfigFile,
|
|
67
|
+
warnings,
|
|
68
|
+
pluginEntry,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(stripJsonComments(await readFile(configResolution.filePath, "utf8")));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
warnings.push(`Unable to safely update existing OpenCode config: ${configResolution.filePath}`);
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
projectWorkflowConfigFile,
|
|
80
|
+
opencodeConfigFile,
|
|
81
|
+
warnings,
|
|
82
|
+
pluginEntry,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
86
|
+
warnings.push(`OpenCode config is not a JSON object: ${configResolution.filePath}`);
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
projectWorkflowConfigFile,
|
|
90
|
+
opencodeConfigFile,
|
|
91
|
+
warnings,
|
|
92
|
+
pluginEntry,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const config = parsed;
|
|
96
|
+
const pluginArray = Array.isArray(config.plugin) ? [...config.plugin] : [];
|
|
97
|
+
if (!pluginArray.includes(pluginEntry)) {
|
|
98
|
+
pluginArray.push(pluginEntry);
|
|
99
|
+
}
|
|
100
|
+
await writeFile(opencodeConfigFile, `${JSON.stringify({
|
|
101
|
+
...parsed,
|
|
102
|
+
plugin: pluginArray,
|
|
103
|
+
}, null, 2)}\n`, "utf8");
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
projectWorkflowConfigFile,
|
|
107
|
+
opencodeConfigFile,
|
|
108
|
+
warnings,
|
|
109
|
+
pluginEntry,
|
|
110
|
+
};
|
|
111
|
+
}
|