@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,220 @@
|
|
|
1
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
const MAX_DOC_CHARS = 12_000;
|
|
4
|
+
const MAX_CANDIDATE_DOCS = 20;
|
|
5
|
+
const MAX_SCAN_DEPTH = 3;
|
|
6
|
+
const DOC_EXTENSIONS = [".md", ".markdown", ".txt", ".rst", ".adoc", ".pdf", ".docx"];
|
|
7
|
+
const COMMON_DOC_DIRS = ["docs", "doc", "spec", "specs", "design", "prd", "product", "requirements"];
|
|
8
|
+
const trimToEmpty = (value) => value?.trim() ?? "";
|
|
9
|
+
const hasDocExtension = (value) => {
|
|
10
|
+
const normalized = value.trim().toLowerCase();
|
|
11
|
+
return DOC_EXTENSIONS.some((extension) => normalized.endsWith(extension));
|
|
12
|
+
};
|
|
13
|
+
const normalizeDocumentLikeInput = (value) => value.trim().replace(/^文档\s*[::]?\s*/i, "");
|
|
14
|
+
const looksLikeDocumentReference = (value) => {
|
|
15
|
+
const normalized = normalizeDocumentLikeInput(value);
|
|
16
|
+
if (!normalized) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (!hasDocExtension(normalized)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return normalized.startsWith("/")
|
|
23
|
+
|| normalized.startsWith("./")
|
|
24
|
+
|| normalized.startsWith("../")
|
|
25
|
+
|| normalized.includes("/")
|
|
26
|
+
|| normalized.includes("\\");
|
|
27
|
+
};
|
|
28
|
+
const parseStructuredRequest = (payload) => {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(payload);
|
|
31
|
+
if (!parsed || typeof parsed !== "object") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const prompt = typeof parsed.prompt === "string"
|
|
35
|
+
? parsed.prompt
|
|
36
|
+
: undefined;
|
|
37
|
+
const projectContext = typeof parsed.projectContext === "string"
|
|
38
|
+
? parsed.projectContext
|
|
39
|
+
: undefined;
|
|
40
|
+
const docPaths = Array.isArray(parsed.docPaths)
|
|
41
|
+
? parsed.docPaths.filter((item) => typeof item === "string")
|
|
42
|
+
: undefined;
|
|
43
|
+
const hasKnownKey = prompt !== undefined || projectContext !== undefined || docPaths !== undefined;
|
|
44
|
+
if (!hasKnownKey) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...(prompt !== undefined ? { prompt } : {}),
|
|
49
|
+
...(projectContext !== undefined ? { projectContext } : {}),
|
|
50
|
+
...(docPaths !== undefined ? { docPaths } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const toAbsolutePath = (pathValue, workspaceRoot) => isAbsolute(pathValue) ? pathValue : resolve(workspaceRoot, pathValue);
|
|
58
|
+
const normalizeDocSnippet = (content) => {
|
|
59
|
+
const normalized = content.replace(/\r\n/g, "\n").trim();
|
|
60
|
+
if (normalized.length <= MAX_DOC_CHARS) {
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
return `${normalized.slice(0, MAX_DOC_CHARS)}\n... (truncated)`;
|
|
64
|
+
};
|
|
65
|
+
const toRelativePath = (workspaceRoot, absolutePath) => {
|
|
66
|
+
const normalizedRoot = workspaceRoot.endsWith("/") ? workspaceRoot : `${workspaceRoot}/`;
|
|
67
|
+
return absolutePath.startsWith(normalizedRoot)
|
|
68
|
+
? absolutePath.slice(normalizedRoot.length)
|
|
69
|
+
: absolutePath;
|
|
70
|
+
};
|
|
71
|
+
const listDocCandidatesFromDir = async (baseDir, workspaceRoot, depth, output) => {
|
|
72
|
+
if (depth > MAX_SCAN_DEPTH || output.length >= MAX_CANDIDATE_DOCS) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await access(baseDir);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const dirEntries = await readdir(baseDir, { withFileTypes: true });
|
|
82
|
+
const sorted = [...dirEntries].sort((a, b) => a.name.localeCompare(b.name));
|
|
83
|
+
for (const entry of sorted) {
|
|
84
|
+
if (output.length >= MAX_CANDIDATE_DOCS) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (entry.name.startsWith(".")) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const childPath = resolve(baseDir, entry.name);
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
if (entry.name === "node_modules" || entry.name === "dist") {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
await listDocCandidatesFromDir(childPath, workspaceRoot, depth + 1, output);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (entry.isFile() && hasDocExtension(entry.name)) {
|
|
99
|
+
output.push(toRelativePath(workspaceRoot, childPath));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const discoverCandidateDocs = async (workspaceRoot) => {
|
|
104
|
+
const candidates = [];
|
|
105
|
+
for (const dirName of COMMON_DOC_DIRS) {
|
|
106
|
+
if (candidates.length >= MAX_CANDIDATE_DOCS) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
const targetDir = resolve(workspaceRoot, dirName);
|
|
110
|
+
await listDocCandidatesFromDir(targetDir, workspaceRoot, 0, candidates);
|
|
111
|
+
}
|
|
112
|
+
return [...new Set(candidates)].slice(0, MAX_CANDIDATE_DOCS);
|
|
113
|
+
};
|
|
114
|
+
const actionHints = [
|
|
115
|
+
"开始",
|
|
116
|
+
"启动",
|
|
117
|
+
"执行",
|
|
118
|
+
"推进",
|
|
119
|
+
"分析",
|
|
120
|
+
"提炼",
|
|
121
|
+
"整理",
|
|
122
|
+
"生成",
|
|
123
|
+
"新增",
|
|
124
|
+
"添加",
|
|
125
|
+
"实现",
|
|
126
|
+
"开发",
|
|
127
|
+
"支持",
|
|
128
|
+
"优化",
|
|
129
|
+
"修复",
|
|
130
|
+
"改造",
|
|
131
|
+
"重构",
|
|
132
|
+
"需求",
|
|
133
|
+
"workflow",
|
|
134
|
+
"工作流",
|
|
135
|
+
"流程",
|
|
136
|
+
"review",
|
|
137
|
+
"plan",
|
|
138
|
+
"develop",
|
|
139
|
+
"test",
|
|
140
|
+
];
|
|
141
|
+
function inferOpenIntent(rawPayload, prompt, docPaths) {
|
|
142
|
+
const lower = rawPayload.toLowerCase();
|
|
143
|
+
const hasActionHint = actionHints.some((hint) => lower.includes(hint.toLowerCase()));
|
|
144
|
+
const onlyDocLikeInput = docPaths.length > 0 && !hasActionHint && prompt.length < 20;
|
|
145
|
+
const explicitDocReference = looksLikeDocumentReference(rawPayload);
|
|
146
|
+
if (explicitDocReference) {
|
|
147
|
+
return { needsClarification: false };
|
|
148
|
+
}
|
|
149
|
+
if (!hasActionHint || onlyDocLikeInput) {
|
|
150
|
+
return {
|
|
151
|
+
needsClarification: true,
|
|
152
|
+
clarificationQuestion: "我看到你给了一个文档,但还不确定你希望我做什么。你想怎么处理?",
|
|
153
|
+
clarificationOptions: [
|
|
154
|
+
"1. 直接启动 workflow",
|
|
155
|
+
"2. 先分析并提炼需求",
|
|
156
|
+
"3. 先补全文档再决定",
|
|
157
|
+
"4. 只看文档,不启动 workflow",
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { needsClarification: false };
|
|
162
|
+
}
|
|
163
|
+
export async function buildWorkflowOpenRequest(payload, workspaceRoot) {
|
|
164
|
+
const rawPayload = trimToEmpty(payload);
|
|
165
|
+
const structured = rawPayload ? parseStructuredRequest(rawPayload) : null;
|
|
166
|
+
const prompt = trimToEmpty(structured?.prompt)
|
|
167
|
+
|| (looksLikeDocumentReference(rawPayload)
|
|
168
|
+
? `请基于这份文档启动 workflow。\n${normalizeDocumentLikeInput(rawPayload)}`
|
|
169
|
+
: rawPayload);
|
|
170
|
+
const structuredDocPaths = (structured?.docPaths ?? []).map((item) => item.trim()).filter((item) => item.length > 0);
|
|
171
|
+
const docPaths = structuredDocPaths;
|
|
172
|
+
const shouldRecallCandidates = !structured && docPaths.length === 0;
|
|
173
|
+
const candidateDocs = shouldRecallCandidates ? await discoverCandidateDocs(workspaceRoot) : [];
|
|
174
|
+
const projectContext = trimToEmpty(structured?.projectContext)
|
|
175
|
+
|| undefined;
|
|
176
|
+
const intent = inferOpenIntent(rawPayload, prompt, docPaths);
|
|
177
|
+
const lines = [];
|
|
178
|
+
lines.push("[USER_PROMPT]");
|
|
179
|
+
lines.push(prompt || "请先基于项目和需求文档完成 spec_refinement。");
|
|
180
|
+
if (projectContext) {
|
|
181
|
+
lines.push("");
|
|
182
|
+
lines.push("[PROJECT_CONTEXT]");
|
|
183
|
+
lines.push(projectContext);
|
|
184
|
+
}
|
|
185
|
+
if (docPaths.length > 0) {
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push("[REFERENCE_DOCS]");
|
|
188
|
+
for (const rawPath of docPaths) {
|
|
189
|
+
const absolutePath = toAbsolutePath(rawPath, workspaceRoot);
|
|
190
|
+
try {
|
|
191
|
+
const content = await readFile(absolutePath, "utf8");
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push(`[DOC_PATH] ${rawPath}`);
|
|
194
|
+
lines.push("[DOC_CONTENT]");
|
|
195
|
+
lines.push(normalizeDocSnippet(content));
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push(`[DOC_PATH] ${rawPath}`);
|
|
201
|
+
lines.push(`[DOC_READ_ERROR] ${message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (candidateDocs.length > 0) {
|
|
206
|
+
lines.push("");
|
|
207
|
+
lines.push("[DOC_CANDIDATES]");
|
|
208
|
+
lines.push(...candidateDocs.map((item) => `- ${item}`));
|
|
209
|
+
lines.push("[DOC_CANDIDATES_POLICY] Candidate list is recall-only. AI must decide relevance and read selected docs before filling artifact.");
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
userRequest: lines.join("\n"),
|
|
213
|
+
prompt,
|
|
214
|
+
docPaths,
|
|
215
|
+
needsClarification: intent.needsClarification,
|
|
216
|
+
...(intent.clarificationQuestion ? { clarificationQuestion: intent.clarificationQuestion } : {}),
|
|
217
|
+
...(intent.clarificationOptions ? { clarificationOptions: intent.clarificationOptions } : {}),
|
|
218
|
+
...(projectContext ? { projectContext } : {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function buildSkillRegistry(skillRoots: string[]): Promise<Map<string, string>>;
|
|
2
|
+
export type SkillRegistryBuildResult = {
|
|
3
|
+
registry: Map<string, string>;
|
|
4
|
+
warnings: string[];
|
|
5
|
+
};
|
|
6
|
+
export declare function buildSkillRegistryWithWarnings(skillRoots: string[]): Promise<SkillRegistryBuildResult>;
|
|
7
|
+
export declare function resolveSkillPaths(registry: Map<string, string>, skillNames: string[]): Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function loadResolvedSkillContents(registry: Map<string, string>, skillNames: string[]): Promise<Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
content: string;
|
|
15
|
+
}>>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { access, readdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, extname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
function expandPath(input) {
|
|
6
|
+
if (input === "~") {
|
|
7
|
+
return homedir();
|
|
8
|
+
}
|
|
9
|
+
if (input.startsWith("~/")) {
|
|
10
|
+
return join(homedir(), input.slice(2));
|
|
11
|
+
}
|
|
12
|
+
return isAbsolute(input) ? input : resolve(input);
|
|
13
|
+
}
|
|
14
|
+
async function fileExists(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
await access(filePath);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function buildSkillRegistry(skillRoots) {
|
|
24
|
+
const registry = new Map();
|
|
25
|
+
for (const rawRoot of skillRoots) {
|
|
26
|
+
const root = expandPath(rawRoot);
|
|
27
|
+
let entries = [];
|
|
28
|
+
try {
|
|
29
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") {
|
|
36
|
+
const skillName = basename(entry.name, ".md");
|
|
37
|
+
registry.set(skillName, join(root, entry.name));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
const skillPath = join(root, entry.name, "SKILL.md");
|
|
42
|
+
if (await fileExists(skillPath)) {
|
|
43
|
+
registry.set(entry.name, skillPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return registry;
|
|
49
|
+
}
|
|
50
|
+
export async function buildSkillRegistryWithWarnings(skillRoots) {
|
|
51
|
+
const registry = new Map();
|
|
52
|
+
const warnings = [];
|
|
53
|
+
for (const rawRoot of skillRoots) {
|
|
54
|
+
const root = expandPath(rawRoot);
|
|
55
|
+
let entries = [];
|
|
56
|
+
try {
|
|
57
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
warnings.push(`Skill root not found or unreadable: ${rawRoot}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") {
|
|
65
|
+
const skillName = basename(entry.name, ".md");
|
|
66
|
+
const skillPath = join(root, entry.name);
|
|
67
|
+
if (registry.has(skillName)) {
|
|
68
|
+
warnings.push(`Duplicate skill name detected: ${skillName}`);
|
|
69
|
+
}
|
|
70
|
+
registry.set(skillName, skillPath);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
const skillPath = join(root, entry.name, "SKILL.md");
|
|
75
|
+
if (await fileExists(skillPath)) {
|
|
76
|
+
if (registry.has(entry.name)) {
|
|
77
|
+
warnings.push(`Duplicate skill name detected: ${entry.name}`);
|
|
78
|
+
}
|
|
79
|
+
registry.set(entry.name, skillPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { registry, warnings };
|
|
85
|
+
}
|
|
86
|
+
export function resolveSkillPaths(registry, skillNames) {
|
|
87
|
+
return skillNames.flatMap((name) => {
|
|
88
|
+
const path = registry.get(name);
|
|
89
|
+
return path ? [{ name, path }] : [];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export async function loadResolvedSkillContents(registry, skillNames) {
|
|
93
|
+
const resolved = resolveSkillPaths(registry, skillNames);
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const skill of resolved) {
|
|
96
|
+
try {
|
|
97
|
+
const content = await readFile(skill.path, "utf8");
|
|
98
|
+
results.push({
|
|
99
|
+
...skill,
|
|
100
|
+
content: content.trim(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type WorkflowConfigPhase = "spec_refinement" | "plan" | "develop" | "review" | "test";
|
|
2
|
+
export type WorkflowPhaseConfig = {
|
|
3
|
+
requiredSkills?: string[];
|
|
4
|
+
};
|
|
5
|
+
export type WorkflowConfigFile = {
|
|
6
|
+
skillRoots?: string[];
|
|
7
|
+
phases?: Partial<Record<WorkflowConfigPhase, WorkflowPhaseConfig>>;
|
|
8
|
+
};
|
|
9
|
+
export type ResolvedWorkflowConfig = {
|
|
10
|
+
skillRoots: string[];
|
|
11
|
+
phases: Partial<Record<WorkflowConfigPhase, WorkflowPhaseConfig>>;
|
|
12
|
+
warnings: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare function resolveWorkflowConfig(args: {
|
|
15
|
+
projectConfigFile: string;
|
|
16
|
+
homeDir?: string;
|
|
17
|
+
}): Promise<ResolvedWorkflowConfig>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readJsonFile } from "../shared/json-file";
|
|
4
|
+
const EMPTY_CONFIG = {
|
|
5
|
+
skillRoots: [],
|
|
6
|
+
phases: {},
|
|
7
|
+
warnings: [],
|
|
8
|
+
};
|
|
9
|
+
function unique(values) {
|
|
10
|
+
return [...new Set(values)];
|
|
11
|
+
}
|
|
12
|
+
function normalizePhaseConfig(config) {
|
|
13
|
+
if (!config || typeof config !== "object") {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const requiredSkills = Array.isArray(config.requiredSkills)
|
|
17
|
+
? unique(config.requiredSkills.map((value) => value.trim()).filter(Boolean))
|
|
18
|
+
: undefined;
|
|
19
|
+
if (!requiredSkills || requiredSkills.length === 0) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return { requiredSkills };
|
|
23
|
+
}
|
|
24
|
+
function mergeConfigs(base, incoming) {
|
|
25
|
+
if (!incoming) {
|
|
26
|
+
return base;
|
|
27
|
+
}
|
|
28
|
+
const next = {
|
|
29
|
+
skillRoots: unique([
|
|
30
|
+
...base.skillRoots,
|
|
31
|
+
...(Array.isArray(incoming.skillRoots)
|
|
32
|
+
? incoming.skillRoots.map((value) => value.trim()).filter(Boolean)
|
|
33
|
+
: []),
|
|
34
|
+
]),
|
|
35
|
+
phases: { ...base.phases },
|
|
36
|
+
warnings: [...base.warnings],
|
|
37
|
+
};
|
|
38
|
+
for (const phase of ["spec_refinement", "plan", "develop", "review", "test"]) {
|
|
39
|
+
const merged = normalizePhaseConfig(incoming.phases?.[phase]) ?? next.phases[phase];
|
|
40
|
+
if (merged) {
|
|
41
|
+
next.phases[phase] = merged;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
export async function resolveWorkflowConfig(args) {
|
|
47
|
+
const globalConfigFile = join(args.homeDir ?? homedir(), ".config", "opencode", "workflow.json");
|
|
48
|
+
const globalConfig = await readJsonFile(globalConfigFile);
|
|
49
|
+
const projectConfig = await readJsonFile(args.projectConfigFile);
|
|
50
|
+
return mergeConfigs(mergeConfigs(EMPTY_CONFIG, globalConfig), projectConfig);
|
|
51
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WorkflowDoctorResult } from "./workflow-doctor";
|
|
2
|
+
import type { WorkflowInstallResult } from "../install/workflow-installer";
|
|
3
|
+
export declare function formatWorkflowDoctorResult(result: WorkflowDoctorResult): string;
|
|
4
|
+
export declare function formatWorkflowInstallResult(result: WorkflowInstallResult): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const divider = "=".repeat(64);
|
|
2
|
+
export function formatWorkflowDoctorResult(result) {
|
|
3
|
+
const lines = [
|
|
4
|
+
divider,
|
|
5
|
+
`Workflow Doctor: ${result.ok ? "OK" : "ATTENTION"}`,
|
|
6
|
+
`Project config: ${result.projectConfigFile}`,
|
|
7
|
+
`Global config: ${result.globalConfigFile}`,
|
|
8
|
+
];
|
|
9
|
+
lines.push("");
|
|
10
|
+
lines.push("Checks:");
|
|
11
|
+
for (const check of result.checks) {
|
|
12
|
+
const marker = check.status === "ok" ? "[ok]" : check.status === "warning" ? "[warn]" : "[error]";
|
|
13
|
+
lines.push(`${marker} ${check.name}: ${check.detail}`);
|
|
14
|
+
}
|
|
15
|
+
lines.push("");
|
|
16
|
+
lines.push("Required skills:");
|
|
17
|
+
if (result.requiredSkills.length === 0) {
|
|
18
|
+
lines.push("- none");
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
for (const entry of result.requiredSkills) {
|
|
22
|
+
lines.push(`- ${entry.phase}: ${entry.skills.join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (result.missingSkills.length > 0) {
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push("Missing skills:");
|
|
28
|
+
for (const entry of result.missingSkills) {
|
|
29
|
+
lines.push(`- ${entry.phase}: ${entry.skill}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (result.nextSteps.length > 0) {
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push("Next steps:");
|
|
35
|
+
for (const step of result.nextSteps) {
|
|
36
|
+
lines.push(`- ${step}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (result.warnings.length > 0) {
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push("Warnings:");
|
|
42
|
+
for (const warning of result.warnings) {
|
|
43
|
+
lines.push(`- ${warning}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
lines.push(divider);
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
export function formatWorkflowInstallResult(result) {
|
|
50
|
+
const lines = [
|
|
51
|
+
divider,
|
|
52
|
+
`Workflow Install: ${result.ok ? "OK" : "ATTENTION"}`,
|
|
53
|
+
`Project workflow.json: ${result.projectWorkflowConfigFile}`,
|
|
54
|
+
`OpenCode config: ${result.opencodeConfigFile}`,
|
|
55
|
+
`Plugin entry: ${result.pluginEntry}`,
|
|
56
|
+
];
|
|
57
|
+
if (result.warnings.length > 0) {
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push("Warnings:");
|
|
60
|
+
for (const warning of result.warnings) {
|
|
61
|
+
lines.push(`- ${warning}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push("Recommended next steps:");
|
|
66
|
+
lines.push("- Run: bun run src/cli.ts doctor");
|
|
67
|
+
lines.push("- Then open OpenCode and use workflow_open / workflow_attach");
|
|
68
|
+
lines.push(divider);
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WorkflowWorkspace } from "../workspace/workflow-workspace";
|
|
2
|
+
export type WorkflowDoctorResult = {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
globalConfigFile: string;
|
|
5
|
+
projectConfigFile: string;
|
|
6
|
+
skillRoots: string[];
|
|
7
|
+
requiredSkills: Array<{
|
|
8
|
+
phase: string;
|
|
9
|
+
skills: string[];
|
|
10
|
+
}>;
|
|
11
|
+
missingSkills: Array<{
|
|
12
|
+
phase: string;
|
|
13
|
+
skill: string;
|
|
14
|
+
}>;
|
|
15
|
+
checks: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
status: "ok" | "warning" | "error";
|
|
18
|
+
detail: string;
|
|
19
|
+
}>;
|
|
20
|
+
nextSteps: string[];
|
|
21
|
+
warnings: string[];
|
|
22
|
+
};
|
|
23
|
+
export declare function runWorkflowDoctor(workspace: WorkflowWorkspace): Promise<WorkflowDoctorResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { buildSkillRegistryWithWarnings } from "../config/skill-registry";
|
|
6
|
+
import { resolveWorkflowConfig } from "../config/workflow-config";
|
|
7
|
+
async function fileExists(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
await access(filePath);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function readTextIfExists(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
return await readFile(filePath, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function gitignoreHasWorkflowHarness(content) {
|
|
25
|
+
return content.split("\n").some((line) => {
|
|
26
|
+
const normalized = line.trim();
|
|
27
|
+
return normalized.includes(".workflow-harness/");
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function runWorkflowDoctor(workspace) {
|
|
31
|
+
const projectConfigFile = workspace.workflowConfigFile();
|
|
32
|
+
const globalConfigFile = join(homedir(), ".config", "opencode", "workflow.json");
|
|
33
|
+
const resolvedConfig = await resolveWorkflowConfig({
|
|
34
|
+
projectConfigFile,
|
|
35
|
+
});
|
|
36
|
+
const registryResult = await buildSkillRegistryWithWarnings(resolvedConfig.skillRoots);
|
|
37
|
+
const checks = [];
|
|
38
|
+
const requiredSkills = Object.entries(resolvedConfig.phases)
|
|
39
|
+
.flatMap(([phase, phaseConfig]) => {
|
|
40
|
+
const skills = phaseConfig?.requiredSkills ?? [];
|
|
41
|
+
return skills.length > 0 ? [{ phase, skills }] : [];
|
|
42
|
+
});
|
|
43
|
+
const warnings = [...resolvedConfig.warnings, ...registryResult.warnings];
|
|
44
|
+
const missingSkills = [];
|
|
45
|
+
for (const { phase, skills } of requiredSkills) {
|
|
46
|
+
for (const skillName of skills) {
|
|
47
|
+
if (!registryResult.registry.has(skillName)) {
|
|
48
|
+
warnings.push(`Missing skill for phase ${phase}: ${skillName}`);
|
|
49
|
+
missingSkills.push({ phase, skill: skillName });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const hasGlobalConfig = await fileExists(globalConfigFile);
|
|
54
|
+
if (!hasGlobalConfig) {
|
|
55
|
+
warnings.push(`Global workflow.json not found: ${globalConfigFile}`);
|
|
56
|
+
}
|
|
57
|
+
const workspaceRoot = workspace.baseDir().endsWith(".workflow-harness")
|
|
58
|
+
? dirname(workspace.baseDir())
|
|
59
|
+
: workspace.baseDir();
|
|
60
|
+
const gitignoreFile = join(workspaceRoot, ".gitignore");
|
|
61
|
+
const gitignoreContent = await readTextIfExists(gitignoreFile);
|
|
62
|
+
const hasGitignoreRule = gitignoreContent ? gitignoreHasWorkflowHarness(gitignoreContent) : false;
|
|
63
|
+
checks.push({
|
|
64
|
+
name: "project-workflow-config",
|
|
65
|
+
status: (await fileExists(projectConfigFile)) ? "ok" : "warning",
|
|
66
|
+
detail: projectConfigFile,
|
|
67
|
+
});
|
|
68
|
+
checks.push({
|
|
69
|
+
name: "global-workflow-config",
|
|
70
|
+
status: hasGlobalConfig ? "ok" : "warning",
|
|
71
|
+
detail: globalConfigFile,
|
|
72
|
+
});
|
|
73
|
+
checks.push({
|
|
74
|
+
name: "skill-roots",
|
|
75
|
+
status: resolvedConfig.skillRoots.length > 0 ? "ok" : "warning",
|
|
76
|
+
detail: resolvedConfig.skillRoots.length > 0 ? resolvedConfig.skillRoots.join(", ") : "No skillRoots configured",
|
|
77
|
+
});
|
|
78
|
+
checks.push({
|
|
79
|
+
name: "required-skills",
|
|
80
|
+
status: missingSkills.length === 0 ? "ok" : "error",
|
|
81
|
+
detail: missingSkills.length === 0
|
|
82
|
+
? "All required skills resolved"
|
|
83
|
+
: missingSkills.map((entry) => `${entry.phase}:${entry.skill}`).join(", "),
|
|
84
|
+
});
|
|
85
|
+
checks.push({
|
|
86
|
+
name: "gitignore-workflow-harness",
|
|
87
|
+
status: hasGitignoreRule ? "ok" : "warning",
|
|
88
|
+
detail: hasGitignoreRule
|
|
89
|
+
? ".workflow-harness runtime files are ignored"
|
|
90
|
+
: "Recommend ignoring .workflow-harness/ or at least runtime subpaths such as workflows/ and plugin-load.json",
|
|
91
|
+
});
|
|
92
|
+
const nextSteps = [];
|
|
93
|
+
if (!(await fileExists(projectConfigFile))) {
|
|
94
|
+
nextSteps.push(`Run installer to generate project workflow.json: ${projectConfigFile}`);
|
|
95
|
+
}
|
|
96
|
+
if (resolvedConfig.skillRoots.length === 0) {
|
|
97
|
+
nextSteps.push("Add skillRoots to workflow.json if you want phase skill injection");
|
|
98
|
+
}
|
|
99
|
+
if (missingSkills.length > 0) {
|
|
100
|
+
nextSteps.push("Fix missing requiredSkills or add corresponding skill files under configured skillRoots");
|
|
101
|
+
}
|
|
102
|
+
if (!hasGlobalConfig) {
|
|
103
|
+
nextSteps.push(`Optional: create global workflow defaults at ${globalConfigFile}`);
|
|
104
|
+
}
|
|
105
|
+
if (!hasGitignoreRule) {
|
|
106
|
+
nextSteps.push("Consider ignoring .workflow-harness/ runtime files to avoid accidental commits");
|
|
107
|
+
}
|
|
108
|
+
const ok = checks.every((check) => check.status !== "error");
|
|
109
|
+
return {
|
|
110
|
+
ok,
|
|
111
|
+
globalConfigFile,
|
|
112
|
+
projectConfigFile,
|
|
113
|
+
skillRoots: resolvedConfig.skillRoots,
|
|
114
|
+
requiredSkills,
|
|
115
|
+
missingSkills,
|
|
116
|
+
checks,
|
|
117
|
+
nextSteps,
|
|
118
|
+
warnings,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WorkflowEngine, WorkflowEngineDeps } from "./workflow-engine";
|
|
2
|
+
export declare class DefaultWorkflowEngine implements WorkflowEngine {
|
|
3
|
+
private readonly deps;
|
|
4
|
+
constructor(deps: WorkflowEngineDeps);
|
|
5
|
+
private isArtifactPhase;
|
|
6
|
+
private buildRefinementDispatchSummary;
|
|
7
|
+
private buildDispatchPrompt;
|
|
8
|
+
tick(workflowId: string): Promise<void>;
|
|
9
|
+
}
|