@oisincoveney/pipeline 2.0.0 → 2.0.1
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/dist/argo-workflow.js +0 -19
- package/dist/cli/program.d.ts +1 -0
- package/dist/cli/program.js +7 -4
- package/dist/cluster-doctor.js +69 -48
- package/dist/config/schemas.d.ts +2 -2
- package/dist/install-commands/opencode.js +422 -0
- package/dist/install-commands/shared.js +23 -0
- package/dist/install-commands.js +7 -440
- package/dist/schedule/backlog-context.js +114 -0
- package/dist/schedule/baseline.js +267 -0
- package/dist/schedule/planner.js +3 -373
- package/package.json +1 -1
package/dist/install-commands.js
CHANGED
|
@@ -1,438 +1,12 @@
|
|
|
1
|
-
import { DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST } from "./config/defaults.js";
|
|
2
|
-
import { resolvePackageAssetPath } from "./package-assets.js";
|
|
3
1
|
import { loadPipelineConfig } from "./config/load.js";
|
|
4
2
|
import "./config.js";
|
|
5
|
-
import { compileWorkflowPlan } from "./workflow-planner.js";
|
|
6
3
|
import { mergeOpenCodeProjectConfig } from "./opencode-project-config.js";
|
|
7
|
-
import {
|
|
4
|
+
import { COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, invocationForHost } from "./install-commands/shared.js";
|
|
5
|
+
import { opencodeDefinitions } from "./install-commands/opencode.js";
|
|
8
6
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
9
|
-
import {
|
|
10
|
-
import matter from "gray-matter";
|
|
7
|
+
import { dirname, join, relative } from "node:path";
|
|
11
8
|
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
12
9
|
//#region src/install-commands.ts
|
|
13
|
-
const GENERATED_MARKER = "<!-- Generated by @oisincoveney/pipeline. -->";
|
|
14
|
-
const GENERATED_TS_MARKER = "// Generated by @oisincoveney/pipeline.";
|
|
15
|
-
const GENERATED_YAML_MARKER = "# Generated by @oisincoveney/pipeline.";
|
|
16
|
-
const OWNER_MARKER_PREFIX = "<!-- @oisincoveney/pipeline:";
|
|
17
|
-
const OWNER_TS_MARKER_PREFIX = "// @oisincoveney/pipeline:";
|
|
18
|
-
const OWNER_YAML_MARKER_PREFIX = "# @oisincoveney/pipeline:";
|
|
19
|
-
const AGENTS_MD_START = "<!-- @oisincoveney/pipeline:agents:start -->";
|
|
20
|
-
const AGENTS_MD_END = "<!-- @oisincoveney/pipeline:agents:end -->";
|
|
21
|
-
const SINGLE_OPENCODE_PLUGIN_ARRAY_RE = /\n {2}"plugin": \[\n {4}("[^"]+")\n {2}\]/;
|
|
22
|
-
const OPENCODE_PROJECT_CONFIG_PATH = ".opencode/opencode.json";
|
|
23
|
-
const OPENCODE_COMMAND_PREFIX = "moka-";
|
|
24
|
-
const ENTRYPOINT_PATH_PATTERNS = { opencode: [/^\.opencode\/commands\/(?:moka-)?([^/]+)\.md$/] };
|
|
25
|
-
const COMMAND_HOSTS = ["opencode"];
|
|
26
|
-
const OPENCODE_ORCHESTRATOR_AGENT_ID = "MoKa Orchestrator";
|
|
27
|
-
const MOKA_PROFILE_PREFIX = "moka-";
|
|
28
|
-
function header(host) {
|
|
29
|
-
return [
|
|
30
|
-
GENERATED_MARKER,
|
|
31
|
-
`${OWNER_MARKER_PREFIX}host=${host} -->`,
|
|
32
|
-
""
|
|
33
|
-
].join("\n");
|
|
34
|
-
}
|
|
35
|
-
function markdown(data, body) {
|
|
36
|
-
return `${matter.stringify(body.trimEnd(), data).trimEnd()}\n`;
|
|
37
|
-
}
|
|
38
|
-
function profileEntries(config) {
|
|
39
|
-
return Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b));
|
|
40
|
-
}
|
|
41
|
-
function entrypointEntries(config) {
|
|
42
|
-
const entries = Object.entries(config.entrypoints);
|
|
43
|
-
return entries.length > 0 ? entries : [["execute", {
|
|
44
|
-
description: "Run the configured pipeline workflow",
|
|
45
|
-
workflow: config.default_workflow
|
|
46
|
-
}]];
|
|
47
|
-
}
|
|
48
|
-
function entrypointDescription(id, entrypoint) {
|
|
49
|
-
return entrypoint.description ?? `Run the ${id} workflow`;
|
|
50
|
-
}
|
|
51
|
-
function entrypointCommandDefinitions(_host, config, makeDefinition) {
|
|
52
|
-
return entrypointEntries(config).map(([id, entrypoint]) => makeDefinition(id, entrypoint));
|
|
53
|
-
}
|
|
54
|
-
function nativeProfileEntries(host, config) {
|
|
55
|
-
return profileEntries(config).filter(([id, profile]) => id !== config.orchestrator?.profile && canRunNatively(host, profile));
|
|
56
|
-
}
|
|
57
|
-
function orchestratorProfile(config) {
|
|
58
|
-
if (!config.orchestrator) return;
|
|
59
|
-
const profile = config.profiles[config.orchestrator.profile];
|
|
60
|
-
if (!profile) throw new Error(`Orchestrator profile '${config.orchestrator.profile}' is not declared.`);
|
|
61
|
-
return { ...profile };
|
|
62
|
-
}
|
|
63
|
-
function resolvedHostModel(config, host, profile) {
|
|
64
|
-
const runner = config.runners[profile.runner];
|
|
65
|
-
const hostRunner = config.runners[host];
|
|
66
|
-
if (profile.host_models?.[host]) return profile.host_models[host];
|
|
67
|
-
if (runner?.host_models?.[host]) return runner.host_models[host];
|
|
68
|
-
if (profile.runner === host) return profile.model ?? runner?.model;
|
|
69
|
-
return hostRunner?.model;
|
|
70
|
-
}
|
|
71
|
-
function canRunNatively(host, profile) {
|
|
72
|
-
if (profile.runner === host) return true;
|
|
73
|
-
return host === "opencode" && isModelRunner(profile.runner);
|
|
74
|
-
}
|
|
75
|
-
function isModelRunner(runnerId) {
|
|
76
|
-
return COMMAND_HOSTS.some((host) => host === runnerId);
|
|
77
|
-
}
|
|
78
|
-
function agentDispatchRoutes(host, config, workflowId = config.default_workflow) {
|
|
79
|
-
return compileWorkflowPlan(config, workflowId).topologicalOrder.flatMap((node) => {
|
|
80
|
-
if (!(node.kind === "agent" && node.profile)) return [];
|
|
81
|
-
const profile = config.profiles[node.profile];
|
|
82
|
-
if (!profile) return [];
|
|
83
|
-
return [dispatchRouteForAgent(host, config, {
|
|
84
|
-
needs: node.needs,
|
|
85
|
-
nodeId: node.id,
|
|
86
|
-
profile,
|
|
87
|
-
profileId: node.profile
|
|
88
|
-
})];
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
function dispatchRouteForAgent(host, config, route) {
|
|
92
|
-
const runnerId = route.profile.runner;
|
|
93
|
-
if (runnerId === host) {
|
|
94
|
-
const model = resolvedHostModel(config, host, route.profile);
|
|
95
|
-
return {
|
|
96
|
-
...route,
|
|
97
|
-
kind: "native-named-agent",
|
|
98
|
-
...model ? { model } : {},
|
|
99
|
-
nativeAgentId: nativeAgentIdForHost(host, route.profileId),
|
|
100
|
-
runnerId
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
if (host === "opencode" && isModelRunner(runnerId)) {
|
|
104
|
-
const model = resolvedHostModel(config, host, route.profile);
|
|
105
|
-
return {
|
|
106
|
-
...route,
|
|
107
|
-
kind: "native-model-agent",
|
|
108
|
-
...model ? { model } : {},
|
|
109
|
-
nativeAgentId: nativeAgentIdForHost(host, route.profileId),
|
|
110
|
-
runnerId
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
...route,
|
|
115
|
-
kind: "cli",
|
|
116
|
-
runnerId
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function nativeAgentIdForHost(host, profileId) {
|
|
120
|
-
return host === "opencode" ? opencodeAgentName(profileId) : profileId;
|
|
121
|
-
}
|
|
122
|
-
function opencodeAgentName(profileId) {
|
|
123
|
-
if (!profileId.startsWith(MOKA_PROFILE_PREFIX)) return profileId;
|
|
124
|
-
return `MoKa ${profileId.slice(5).split("-").map(opencodeAgentNamePart).join(" ")}`;
|
|
125
|
-
}
|
|
126
|
-
function opencodeAgentNamePart(part) {
|
|
127
|
-
if (part === "opencode") return "OpenCode";
|
|
128
|
-
return `${part.charAt(0).toUpperCase()}${part.slice(1)}`;
|
|
129
|
-
}
|
|
130
|
-
function grants(actor) {
|
|
131
|
-
return [
|
|
132
|
-
`model: ${actor.model ?? "default"}`,
|
|
133
|
-
`tools: ${(actor.tools ?? []).join(", ") || "none"}`,
|
|
134
|
-
`rules: ${(actor.rules ?? []).join(", ") || "none"}`,
|
|
135
|
-
`skills: ${(actor.skills ?? []).join(", ") || "none"}`,
|
|
136
|
-
`mcp_servers: ${(actor.mcp_servers ?? []).join(", ") || "none"}`,
|
|
137
|
-
`filesystem: ${actor.filesystem?.mode ?? "default"}`,
|
|
138
|
-
`network: ${actor.network?.mode ?? "default"}`,
|
|
139
|
-
..."output" in actor ? [`output: ${actor.output?.format ?? "text"}`] : []
|
|
140
|
-
].join("\n");
|
|
141
|
-
}
|
|
142
|
-
function orchestratorBlock(config) {
|
|
143
|
-
const profile = orchestratorProfile(config);
|
|
144
|
-
if (!profile) return "Configured orchestrator: none";
|
|
145
|
-
return [
|
|
146
|
-
"Configured orchestrator:",
|
|
147
|
-
grants(profile),
|
|
148
|
-
`hooks: ${Object.keys(config.hooks.functions).join(", ") || "none"}`,
|
|
149
|
-
"",
|
|
150
|
-
instructionsPointer(profile)
|
|
151
|
-
].join("\n");
|
|
152
|
-
}
|
|
153
|
-
function dispatchBlock(host, config, workflowId = config.default_workflow) {
|
|
154
|
-
const routes = agentDispatchRoutes(host, config, workflowId);
|
|
155
|
-
if (routes.length === 0) return;
|
|
156
|
-
const plan = compileWorkflowPlan(config, workflowId);
|
|
157
|
-
const nativeRoutes = routes.filter((route) => route.kind !== "cli");
|
|
158
|
-
const cliRoutes = routes.filter((route) => route.kind === "cli");
|
|
159
|
-
return [
|
|
160
|
-
`Run workflow \`${plan.workflowId}\` for the user task.`,
|
|
161
|
-
"",
|
|
162
|
-
nativeDispatchBlock(host, nativeRoutes),
|
|
163
|
-
cliDispatchBlock(host, cliRoutes),
|
|
164
|
-
nodePromptContract(plan.workflowId, routes),
|
|
165
|
-
"Only package-configured gates are blocking. Do not invent RED, GREEN, full-suite, typecheck, or unrelated-drift gates.",
|
|
166
|
-
"If a node returns targeted evidence and has no configured blocking gate, advance to the next node.",
|
|
167
|
-
"Do not bypass configured runner subprocesses or package-configured gates when executing nodes.",
|
|
168
|
-
"Use the listed Task tool routes for native nodes, and run nodes with satisfied dependencies in parallel whenever the host supports concurrent subagent work.",
|
|
169
|
-
hostSpecificDispatchGuard(host, nativeRoutes, cliRoutes)
|
|
170
|
-
].filter((line) => Boolean(line)).join("\n");
|
|
171
|
-
}
|
|
172
|
-
function entrypointDispatchBlock(host, config, id, entrypoint) {
|
|
173
|
-
if ("workflow" in entrypoint) return dispatchBlock(host, config, entrypoint.workflow);
|
|
174
|
-
return [
|
|
175
|
-
`Generate a schedule for entrypoint \`${id}\` and the user task.`,
|
|
176
|
-
`The schedule policy is \`${entrypoint.schedule}\`.`,
|
|
177
|
-
id === "quick" ? "Run `moka submit --quick <task description>` to submit the graph as an Argo Workflow." : `Run \`moka submit <task description>\` to submit the \`${id}\` graph as an Argo Workflow.`,
|
|
178
|
-
"The pipeline runtime executes as Argo DAG tasks using the package-owned runner image.",
|
|
179
|
-
"Configure the target in `~/.config/moka/config.yaml`; use `--kubeconfig <path>` and `--namespace <namespace>` only for explicit command overrides.",
|
|
180
|
-
"Use `moka submit --schedule <schedule.yaml> <task description>` only when rerunning an existing schedule artifact."
|
|
181
|
-
].join("\n");
|
|
182
|
-
}
|
|
183
|
-
function scheduledEntrypointK8sNote(entrypoint) {
|
|
184
|
-
if ("workflow" in entrypoint) return;
|
|
185
|
-
return "Submit Momokaya work as Argo Workflows through `moka submit` and `moka submit --quick`.";
|
|
186
|
-
}
|
|
187
|
-
function orchestratorEntrypointDispatchBlock(host, config) {
|
|
188
|
-
const scheduledEntrypoints = entrypointEntries(config).filter(([, entrypoint]) => !("workflow" in entrypoint));
|
|
189
|
-
if (scheduledEntrypoints.length === 0) return dispatchBlock(host, config);
|
|
190
|
-
return scheduledEntrypoints.map(([id, entrypoint]) => entrypointDispatchBlock(host, config, id, entrypoint)).filter((block) => Boolean(block)).join("\n\n");
|
|
191
|
-
}
|
|
192
|
-
function nativeDispatchBlock(host, routes) {
|
|
193
|
-
if (routes.length === 0) return;
|
|
194
|
-
return [
|
|
195
|
-
`${hostDisplayName(host)} native routes:`,
|
|
196
|
-
...routes.map(nativeDispatchLine),
|
|
197
|
-
""
|
|
198
|
-
].join("\n");
|
|
199
|
-
}
|
|
200
|
-
function nativeDispatchLine(route) {
|
|
201
|
-
const needs = needsSummary(route.needs);
|
|
202
|
-
const model = route.model ? ` model=${route.model}` : "";
|
|
203
|
-
return `- ${route.nodeId}: Task tool subagent_type=${route.nativeAgentId}${model} runner=${route.runnerId} needs=${needs}`;
|
|
204
|
-
}
|
|
205
|
-
function cliDispatchBlock(host, routes) {
|
|
206
|
-
if (routes.length === 0) return;
|
|
207
|
-
return [
|
|
208
|
-
`These nodes are not ${hostDisplayName(host)} native routes.`,
|
|
209
|
-
"CLI routes:",
|
|
210
|
-
...routes.map(cliDispatchLine),
|
|
211
|
-
""
|
|
212
|
-
].join("\n");
|
|
213
|
-
}
|
|
214
|
-
function cliDispatchLine(route) {
|
|
215
|
-
return `- ${route.nodeId}: ${route.runnerId} CLI profile=${route.profileId} command=\`${runnerCliCommand(route)}\` needs=${needsSummary(route.needs)}`;
|
|
216
|
-
}
|
|
217
|
-
function runnerCliCommand(route) {
|
|
218
|
-
if (route.runnerId === "opencode") return `opencode run --agent "${opencodeAgentName(route.profileId)}" --format json --dir <repo-root> <node prompt>`;
|
|
219
|
-
throw new Error(`runner '${route.runnerId}' cannot be represented as a supported native or CLI route`);
|
|
220
|
-
}
|
|
221
|
-
function nodePromptContract(workflowId, routes) {
|
|
222
|
-
return [
|
|
223
|
-
routes.some((route) => route.kind === "cli") ? "For each CLI node prompt include:" : "For each native node prompt include:",
|
|
224
|
-
"- user task",
|
|
225
|
-
`- workflow id: ${workflowId}`,
|
|
226
|
-
"- node id",
|
|
227
|
-
"- profile id",
|
|
228
|
-
"- runner id",
|
|
229
|
-
"- profile instructions reference",
|
|
230
|
-
"- profile grants",
|
|
231
|
-
"- dependency outputs",
|
|
232
|
-
""
|
|
233
|
-
].join("\n");
|
|
234
|
-
}
|
|
235
|
-
function hostSpecificDispatchGuard(host, nativeRoutes, cliRoutes) {
|
|
236
|
-
if (cliRoutes.length > 0 && nativeRoutes.length > 0) return `Do not claim CLI routes are ${hostDisplayName(host)} native routes.`;
|
|
237
|
-
if (cliRoutes.length > 0 && nativeRoutes.length === 0) return `Do not claim these nodes are ${hostDisplayName(host)} subagents.`;
|
|
238
|
-
}
|
|
239
|
-
function hostDisplayName(host) {
|
|
240
|
-
return { opencode: "OpenCode" }[host];
|
|
241
|
-
}
|
|
242
|
-
function needsSummary(needs) {
|
|
243
|
-
return needs.length > 0 ? needs.join(",") : "none";
|
|
244
|
-
}
|
|
245
|
-
function compactLines(lines) {
|
|
246
|
-
return lines.filter((line) => line !== void 0);
|
|
247
|
-
}
|
|
248
|
-
const OPENCODE_PERMISSION_TOOLS = [
|
|
249
|
-
"bash",
|
|
250
|
-
"edit",
|
|
251
|
-
"glob",
|
|
252
|
-
"grep",
|
|
253
|
-
"list",
|
|
254
|
-
"read",
|
|
255
|
-
"write"
|
|
256
|
-
];
|
|
257
|
-
function opencodePermission(actor, options = {}) {
|
|
258
|
-
const allowed = new Set(actor.tools ?? []);
|
|
259
|
-
return {
|
|
260
|
-
...opencodeToolPermissions(allowed),
|
|
261
|
-
external_directory: "deny",
|
|
262
|
-
lsp: "allow",
|
|
263
|
-
skill: opencodeSkillPermission(actor.skills ?? []),
|
|
264
|
-
task: opencodeTaskPermission(allowed, options.allowedTaskAgents ?? [])
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
function opencodeToolPermissions(allowed) {
|
|
268
|
-
return Object.fromEntries(OPENCODE_PERMISSION_TOOLS.map((tool) => [tool, allowed.has(tool) ? "allow" : "deny"]));
|
|
269
|
-
}
|
|
270
|
-
function opencodeSkillPermission(skills) {
|
|
271
|
-
return namedOpencodePermissionMap(skills);
|
|
272
|
-
}
|
|
273
|
-
function opencodeTaskPermission(allowed, allowedTaskAgents) {
|
|
274
|
-
return allowedTaskAgents.length > 0 ? namedOpencodePermissionMap(allowedTaskAgents) : toolPermission(allowed, "task");
|
|
275
|
-
}
|
|
276
|
-
function namedOpencodePermissionMap(names) {
|
|
277
|
-
return names.length > 0 ? {
|
|
278
|
-
"*": "deny",
|
|
279
|
-
...Object.fromEntries(names.map((name) => [name, "allow"]))
|
|
280
|
-
} : "deny";
|
|
281
|
-
}
|
|
282
|
-
function toolPermission(allowed, tool) {
|
|
283
|
-
return allowed.has(tool) ? "allow" : "deny";
|
|
284
|
-
}
|
|
285
|
-
function renderOpenCodeProjectConfig(config) {
|
|
286
|
-
return formatOpenCodeProjectJson({
|
|
287
|
-
...config.mcp_gateway ? JSON.parse(renderOpenCodeGatewayConfig(config)) : { $schema: "https://opencode.ai/config.json" },
|
|
288
|
-
lsp: true,
|
|
289
|
-
...opencodePluginConfig()
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
function opencodePluginConfig() {
|
|
293
|
-
const plugins = DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST.ecosystem_code.flatMap((item) => npmPluginPackage(item)).sort((a, b) => a.localeCompare(b));
|
|
294
|
-
return plugins.length > 0 ? { plugin: plugins } : {};
|
|
295
|
-
}
|
|
296
|
-
function npmPluginPackage(item) {
|
|
297
|
-
if (item.plugin?.kind === "npm") return [item.plugin.package];
|
|
298
|
-
return [];
|
|
299
|
-
}
|
|
300
|
-
function formatOpenCodeProjectJson(value) {
|
|
301
|
-
return `${JSON.stringify(value, null, 2).replace(SINGLE_OPENCODE_PLUGIN_ARRAY_RE, "\n \"plugin\": [$1]")}\n`;
|
|
302
|
-
}
|
|
303
|
-
function localPluginDefinitions() {
|
|
304
|
-
return DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST.ecosystem_code.flatMap((item) => {
|
|
305
|
-
if (item.plugin?.kind !== "local") return [];
|
|
306
|
-
const plugin = readFileSync(resolvePackageAssetPath(item.plugin.source_path), "utf8").trimEnd();
|
|
307
|
-
return [{
|
|
308
|
-
content: [
|
|
309
|
-
GENERATED_TS_MARKER,
|
|
310
|
-
`${OWNER_TS_MARKER_PREFIX}host=opencode`,
|
|
311
|
-
"",
|
|
312
|
-
plugin,
|
|
313
|
-
""
|
|
314
|
-
].join("\n"),
|
|
315
|
-
host: "opencode",
|
|
316
|
-
invocation: invocationForHost("opencode"),
|
|
317
|
-
path: item.plugin.target_path
|
|
318
|
-
}];
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
function opencodeDefinitions(config, cwd) {
|
|
322
|
-
const orchestrator = orchestratorProfile(config);
|
|
323
|
-
return [
|
|
324
|
-
...entrypointCommandDefinitions("opencode", config, (id, entrypoint) => ({
|
|
325
|
-
content: markdown({
|
|
326
|
-
...orchestrator ? { agent: OPENCODE_ORCHESTRATOR_AGENT_ID } : {},
|
|
327
|
-
description: entrypointDescription(id, entrypoint)
|
|
328
|
-
}, compactLines([
|
|
329
|
-
header("opencode").trimEnd(),
|
|
330
|
-
"",
|
|
331
|
-
`Invoke this command with \`${invocationForHost("opencode", id)}\`.`,
|
|
332
|
-
"",
|
|
333
|
-
orchestratorBlock(config),
|
|
334
|
-
"",
|
|
335
|
-
scheduledEntrypointK8sNote(entrypoint),
|
|
336
|
-
scheduledEntrypointK8sNote(entrypoint) ? "" : void 0,
|
|
337
|
-
entrypointDispatchBlock("opencode", config, id, entrypoint)
|
|
338
|
-
]).join("\n")),
|
|
339
|
-
host: "opencode",
|
|
340
|
-
invocation: invocationForHost("opencode", id),
|
|
341
|
-
path: `.opencode/commands/${commandIdForHost("opencode", id)}.md`
|
|
342
|
-
})),
|
|
343
|
-
{
|
|
344
|
-
content: renderOpenCodeProjectConfig(config),
|
|
345
|
-
host: "opencode",
|
|
346
|
-
invocation: invocationForHost("opencode"),
|
|
347
|
-
path: ".opencode/opencode.json"
|
|
348
|
-
},
|
|
349
|
-
...orchestrator ? [{
|
|
350
|
-
content: markdown({
|
|
351
|
-
description: "Orchestrate the configured pipeline and enforce gates.",
|
|
352
|
-
mode: "primary",
|
|
353
|
-
name: OPENCODE_ORCHESTRATOR_AGENT_ID,
|
|
354
|
-
permission: opencodePermission(orchestrator, { allowedTaskAgents: agentDispatchRoutes("opencode", config).filter((route) => route.kind !== "cli").map((route) => route.nativeAgentId).filter((id) => Boolean(id)) })
|
|
355
|
-
}, compactLines([
|
|
356
|
-
header("opencode").trimEnd(),
|
|
357
|
-
"",
|
|
358
|
-
orchestratorBlock(config),
|
|
359
|
-
"",
|
|
360
|
-
orchestratorEntrypointDispatchBlock("opencode", config)
|
|
361
|
-
]).join("\n")),
|
|
362
|
-
host: "opencode",
|
|
363
|
-
invocation: invocationForHost("opencode"),
|
|
364
|
-
path: `.opencode/agents/${OPENCODE_ORCHESTRATOR_AGENT_ID}.md`
|
|
365
|
-
}] : [],
|
|
366
|
-
...nativeProfileEntries("opencode", config).map(([id, profile]) => ({
|
|
367
|
-
content: markdown({
|
|
368
|
-
name: nativeAgentIdForHost("opencode", id),
|
|
369
|
-
description: profile.description ?? id,
|
|
370
|
-
hidden: false,
|
|
371
|
-
mode: "all",
|
|
372
|
-
...opencodeModelProjection(config, profile),
|
|
373
|
-
permission: opencodePermission(profile)
|
|
374
|
-
}, [
|
|
375
|
-
header("opencode").trimEnd(),
|
|
376
|
-
"",
|
|
377
|
-
profile.description ?? id,
|
|
378
|
-
"",
|
|
379
|
-
"Configured grants:",
|
|
380
|
-
grants(profile),
|
|
381
|
-
"",
|
|
382
|
-
instructionsPointer(profile)
|
|
383
|
-
].join("\n")),
|
|
384
|
-
host: "opencode",
|
|
385
|
-
invocation: invocationForHost("opencode"),
|
|
386
|
-
path: `.opencode/agents/${nativeAgentIdForHost("opencode", id)}.md`
|
|
387
|
-
})),
|
|
388
|
-
...localPluginDefinitions(),
|
|
389
|
-
projectAgentsMdDefinition(cwd, "opencode")
|
|
390
|
-
];
|
|
391
|
-
}
|
|
392
|
-
function projectAgentsMdDefinition(cwd, host) {
|
|
393
|
-
const repoName = basename(cwd);
|
|
394
|
-
return {
|
|
395
|
-
block: {
|
|
396
|
-
end: AGENTS_MD_END,
|
|
397
|
-
start: AGENTS_MD_START
|
|
398
|
-
},
|
|
399
|
-
content: [
|
|
400
|
-
AGENTS_MD_START,
|
|
401
|
-
GENERATED_MARKER,
|
|
402
|
-
`${OWNER_MARKER_PREFIX}host=opencode -->`,
|
|
403
|
-
"",
|
|
404
|
-
"## Pipeline Guidance",
|
|
405
|
-
"",
|
|
406
|
-
"This repository uses package-owned `@oisincoveney/pipeline` config.",
|
|
407
|
-
"",
|
|
408
|
-
"- Use `/moka-quick`, `/moka-execute`, or `/moka-inspect` for OpenCode slash-command entrypoints when available.",
|
|
409
|
-
"- Load and follow the relevant skill from `.agents/skills` before doing specialized work.",
|
|
410
|
-
"- Prefer the package-defined pipeline profiles and generated command surfaces over ad hoc subagent prompts.",
|
|
411
|
-
"",
|
|
412
|
-
"## Pipeline Memory",
|
|
413
|
-
"",
|
|
414
|
-
`Use Qdrant collection \`${repoName}\` for this repository.`,
|
|
415
|
-
"",
|
|
416
|
-
`- Call \`qdrant-find\` before research with \`collection_name: ${repoName}\` unless the user explicitly disables memory.`,
|
|
417
|
-
`- Call \`qdrant-store\` during LEARN with \`collection_name: ${repoName}\` for durable lessons worth reusing.`,
|
|
418
|
-
"- Include metadata with at least `repo`, `phase`, `workflow` or `entrypoint`, `task`, and `outcome` when storing lessons.",
|
|
419
|
-
"",
|
|
420
|
-
AGENTS_MD_END,
|
|
421
|
-
""
|
|
422
|
-
].join("\n"),
|
|
423
|
-
host,
|
|
424
|
-
invocation: invocationForHost(host),
|
|
425
|
-
path: "AGENTS.md"
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
function opencodeModelProjection(config, profile) {
|
|
429
|
-
const model = resolvedHostModel(config, "opencode", profile);
|
|
430
|
-
return model ? { model } : {};
|
|
431
|
-
}
|
|
432
|
-
function instructionsPointer(actor) {
|
|
433
|
-
if (actor.instructions.path) return `Instructions: ${actor.instructions.path}`;
|
|
434
|
-
return `Instructions:\n${actor.instructions.inline ?? ""}`;
|
|
435
|
-
}
|
|
436
10
|
function definitionsFor(host, config, cwd) {
|
|
437
11
|
const definitions = { opencode: () => opencodeDefinitions(config, cwd) };
|
|
438
12
|
return dedupeDefinitionsByPath((host === "all" ? COMMAND_HOSTS : [host]).flatMap((name) => definitions[name]()));
|
|
@@ -464,7 +38,7 @@ async function listFiles(root) {
|
|
|
464
38
|
}))).flat();
|
|
465
39
|
}
|
|
466
40
|
function generatedHostFor(content) {
|
|
467
|
-
return COMMAND_HOSTS.find((host) => content.includes(
|
|
41
|
+
return COMMAND_HOSTS.find((host) => content.includes(`<!-- @oisincoveney/pipeline:host=${host} -->`) || content.includes(`// @oisincoveney/pipeline:host=${host}`) || content.includes(`# @oisincoveney/pipeline:host=${host}`));
|
|
468
42
|
}
|
|
469
43
|
async function obsoleteGeneratedItems(cwd, host, wantedPaths) {
|
|
470
44
|
const hosts = new Set(selectedHosts(host));
|
|
@@ -488,15 +62,8 @@ function entrypointIdFromGeneratedPath(host, path) {
|
|
|
488
62
|
if (match) return match[1];
|
|
489
63
|
}
|
|
490
64
|
}
|
|
491
|
-
function invocationForHost(host, entrypointId = "execute") {
|
|
492
|
-
return `${{ opencode: "/" }[host]}${commandIdForHost(host, entrypointId)} <task description>`;
|
|
493
|
-
}
|
|
494
|
-
function commandIdForHost(host, entrypointId) {
|
|
495
|
-
if (host === "opencode") return `${OPENCODE_COMMAND_PREFIX}${entrypointId}`;
|
|
496
|
-
return entrypointId;
|
|
497
|
-
}
|
|
498
65
|
function resolveDefinitionContent(definition, target) {
|
|
499
|
-
if (definition.path !==
|
|
66
|
+
if (definition.path !== ".opencode/opencode.json" || !existsSync(target)) return {
|
|
500
67
|
conflict: false,
|
|
501
68
|
content: definition.content
|
|
502
69
|
};
|
|
@@ -519,7 +86,7 @@ function actionFor(path, content, force, block) {
|
|
|
519
86
|
return "update";
|
|
520
87
|
}
|
|
521
88
|
if (current === content) return "unchanged";
|
|
522
|
-
if (!(current.includes(
|
|
89
|
+
if (!(current.includes("<!-- Generated by @oisincoveney/pipeline. -->") || current.includes("// Generated by @oisincoveney/pipeline.") || current.includes("# Generated by @oisincoveney/pipeline.") || force)) return "conflict";
|
|
523
90
|
return "update";
|
|
524
91
|
}
|
|
525
92
|
function upsertGeneratedBlock(current, content, block) {
|
|
@@ -536,7 +103,7 @@ function upsertGeneratedBlock(current, content, block) {
|
|
|
536
103
|
}
|
|
537
104
|
function installActionForDefinition(definition, target, resolved, force) {
|
|
538
105
|
if (resolved.conflict) return "conflict";
|
|
539
|
-
return actionFor(target, resolved.content, force || definition.path ===
|
|
106
|
+
return actionFor(target, resolved.content, force || definition.path === ".opencode/opencode.json", definition.block);
|
|
540
107
|
}
|
|
541
108
|
async function writeDefinition(definition, target, content) {
|
|
542
109
|
await mkdir(dirname(target), { recursive: true });
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { extractTicketIds } from "../task-ref.js";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Graph, alg } from "@dagrejs/graphlib";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
//#region src/schedule/backlog-context.ts
|
|
7
|
+
const DESCRIPTION_SECTION_RE = /## Description\s+([\s\S]*?)(?=\n## |\s*$)/;
|
|
8
|
+
const ACCEPTANCE_SECTION_RE = /## Acceptance Criteria\s+([\s\S]*?)(?=\n## |\s*$)/;
|
|
9
|
+
const ACCEPTANCE_ITEM_RE = /^\s*-\s*\[[ xX]\]\s*#?([\w.-]+)\s+(.+)$/;
|
|
10
|
+
const LINE_RE = /\r?\n/;
|
|
11
|
+
function loadBacklogPlanningContext(task, worktreePath) {
|
|
12
|
+
const ticketIds = extractTicketIds(task);
|
|
13
|
+
if (ticketIds.length === 0) return {
|
|
14
|
+
parentWorkUnits: [],
|
|
15
|
+
workUnits: []
|
|
16
|
+
};
|
|
17
|
+
const tasks = readBacklogTasks(worktreePath);
|
|
18
|
+
const tasksById = new Map(tasks.map((taskFile) => [taskFile.id, taskFile]));
|
|
19
|
+
const taskGraph = backlogTaskGraph(tasks);
|
|
20
|
+
const parentWorkUnits = [];
|
|
21
|
+
const workUnits = [];
|
|
22
|
+
const parentIds = /* @__PURE__ */ new Set();
|
|
23
|
+
const workUnitIds = /* @__PURE__ */ new Set();
|
|
24
|
+
for (const ticketId of ticketIds) {
|
|
25
|
+
const taskFile = tasksById.get(ticketId);
|
|
26
|
+
if (!taskFile) continue;
|
|
27
|
+
const descendants = descendantBacklogTasks(ticketId, taskGraph);
|
|
28
|
+
if (descendants.length === 0) {
|
|
29
|
+
addUniqueWorkUnit(taskFile.workUnit, workUnits, workUnitIds);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
addUniqueWorkUnit(taskFile.workUnit, parentWorkUnits, parentIds);
|
|
33
|
+
for (const descendant of descendants) addUniqueWorkUnit(descendant.workUnit, workUnits, workUnitIds);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
parentWorkUnits,
|
|
37
|
+
workUnits
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function backlogTaskGraph(tasks) {
|
|
41
|
+
const graph = new Graph();
|
|
42
|
+
const sortedTasks = [...tasks].sort(compareBacklogTaskIds);
|
|
43
|
+
for (const task of sortedTasks) graph.setNode(task.id, task);
|
|
44
|
+
for (const task of sortedTasks) if (task.parentTaskId && graph.hasNode(task.parentTaskId)) graph.setEdge(task.parentTaskId, task.id);
|
|
45
|
+
return graph;
|
|
46
|
+
}
|
|
47
|
+
function descendantBacklogTasks(taskId, taskGraph) {
|
|
48
|
+
if (!taskGraph.hasNode(taskId)) return [];
|
|
49
|
+
return alg.preorder(taskGraph, taskId).slice(1).map((id) => taskGraph.node(id)).filter((task) => Boolean(task));
|
|
50
|
+
}
|
|
51
|
+
function compareBacklogTaskIds(a, b) {
|
|
52
|
+
return a.id.localeCompare(b.id, void 0, { numeric: true });
|
|
53
|
+
}
|
|
54
|
+
function addUniqueWorkUnit(workUnit, target, seen) {
|
|
55
|
+
if (seen.has(workUnit.id)) return;
|
|
56
|
+
seen.add(workUnit.id);
|
|
57
|
+
target.push(workUnit);
|
|
58
|
+
}
|
|
59
|
+
function readBacklogTasks(worktreePath) {
|
|
60
|
+
const tasksDir = join(worktreePath, "backlog", "tasks");
|
|
61
|
+
if (!existsSync(tasksDir)) return [];
|
|
62
|
+
return readdirSync(tasksDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).flatMap((entry) => readBacklogTaskFile(join(tasksDir, entry.name)));
|
|
63
|
+
}
|
|
64
|
+
function readBacklogTaskFile(path) {
|
|
65
|
+
const parsed = matter(readFileSync(path, "utf8"));
|
|
66
|
+
const id = stringFrontmatter(parsed.data.id);
|
|
67
|
+
if (!id) return [];
|
|
68
|
+
return [{
|
|
69
|
+
id,
|
|
70
|
+
parentTaskId: stringFrontmatter(parsed.data.parent_task_id),
|
|
71
|
+
workUnit: {
|
|
72
|
+
acceptance_criteria: acceptanceCriteriaFromMarkdown(parsed.content),
|
|
73
|
+
...optionalStringArrayField("dependencies", stringArrayFrontmatter(parsed.data.dependencies)),
|
|
74
|
+
...optionalStringField("description", descriptionFromMarkdown(parsed.content)),
|
|
75
|
+
id,
|
|
76
|
+
...optionalStringField("title", stringFrontmatter(parsed.data.title))
|
|
77
|
+
}
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
80
|
+
function stringFrontmatter(value) {
|
|
81
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
82
|
+
}
|
|
83
|
+
function stringArrayFrontmatter(value) {
|
|
84
|
+
if (!Array.isArray(value)) return [];
|
|
85
|
+
return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
function optionalStringField(key, value) {
|
|
88
|
+
return value ? { [key]: value } : {};
|
|
89
|
+
}
|
|
90
|
+
function optionalStringArrayField(key, value) {
|
|
91
|
+
return value.length > 0 ? { [key]: value } : {};
|
|
92
|
+
}
|
|
93
|
+
function descriptionFromMarkdown(content) {
|
|
94
|
+
const marked = betweenMarkers(content, "<!-- SECTION:DESCRIPTION:BEGIN -->", "<!-- SECTION:DESCRIPTION:END -->");
|
|
95
|
+
if (marked) return marked;
|
|
96
|
+
return cleanupMarkdownSection(content.match(DESCRIPTION_SECTION_RE)?.[1]);
|
|
97
|
+
}
|
|
98
|
+
function acceptanceCriteriaFromMarkdown(content) {
|
|
99
|
+
return (betweenMarkers(content, "<!-- AC:BEGIN -->", "<!-- AC:END -->") ?? content.match(ACCEPTANCE_SECTION_RE)?.[1] ?? "").split(LINE_RE).map((line) => line.match(ACCEPTANCE_ITEM_RE)).filter((match) => Boolean(match)).map((match) => ({
|
|
100
|
+
id: match[1] ?? "",
|
|
101
|
+
text: (match[2] ?? "").trim()
|
|
102
|
+
})).filter((criterion) => criterion.id && criterion.text);
|
|
103
|
+
}
|
|
104
|
+
function betweenMarkers(content, start, end) {
|
|
105
|
+
const startIndex = content.indexOf(start);
|
|
106
|
+
const endIndex = content.indexOf(end);
|
|
107
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) return;
|
|
108
|
+
return cleanupMarkdownSection(content.slice(startIndex + start.length, endIndex));
|
|
109
|
+
}
|
|
110
|
+
function cleanupMarkdownSection(value) {
|
|
111
|
+
return value?.split(LINE_RE).map((line) => line.trim()).filter(Boolean).join("\n") || void 0;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { loadBacklogPlanningContext };
|