@oisincoveney/pipeline 2.7.0 → 2.8.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/defaults/pipeline.yaml +25 -0
- package/dist/argo-graph.js +7 -7
- package/dist/argo-submit.d.ts +2 -4
- package/dist/argo-submit.js +80 -80
- package/dist/bench/eval-report.js +27 -0
- package/dist/cli/program.js +19 -3
- package/dist/cluster-doctor.js +89 -101
- package/dist/commands/bench-command.js +18 -0
- package/dist/config/defaults.js +9 -19
- package/dist/config/load.js +47 -37
- package/dist/config/schemas.d.ts +24 -7
- package/dist/config/schemas.js +20 -7
- package/dist/context/repo-map.js +203 -0
- package/dist/install-commands/opencode.js +10 -1
- package/dist/mcp/gateway-error.js +15 -0
- package/dist/mcp/gateway.js +119 -220
- package/dist/moka-global-config.js +20 -20
- package/dist/moka-submit.d.ts +6 -6
- package/dist/pipeline-init.js +18 -12
- package/dist/pipeline-runtime.js +592 -372
- package/dist/planning/compile.d.ts +8 -3
- package/dist/planning/compile.js +7 -7
- package/dist/planning/generate.d.ts +6 -1
- package/dist/planning/generate.js +29 -7
- package/dist/run-state/git-refs.js +124 -94
- package/dist/runner-command-contract.d.ts +6 -1
- package/dist/runner-command-contract.js +6 -5
- package/dist/runner-event-schema.d.ts +6 -6
- package/dist/runner-event-sink.js +37 -68
- package/dist/runner.d.ts +6 -1
- package/dist/runner.js +3 -3
- package/dist/runtime/agent-node/agent-node.js +218 -159
- package/dist/runtime/changed-files/changed-files.js +15 -27
- package/dist/runtime/changed-files/index.js +2 -0
- package/dist/runtime/drain-merge/drain-merge.js +124 -82
- package/dist/runtime/gates/gates.js +45 -27
- package/dist/runtime/hooks/hooks.js +74 -29
- package/dist/runtime/local-scheduler.js +45 -0
- package/dist/runtime/opencode-server.js +32 -23
- package/dist/runtime/opencode-session-executor.js +101 -44
- package/dist/runtime/parallel-node/parallel-node.js +93 -75
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
- package/dist/runtime/run-journal.js +21 -0
- package/dist/runtime/scheduler.js +122 -93
- package/dist/runtime/select-candidate/select-candidate.js +52 -24
- package/dist/runtime/services/agent-node-runtime-service.js +15 -0
- package/dist/runtime/services/command-executor-service.js +8 -0
- package/dist/runtime/services/config-io-service.js +42 -0
- package/dist/runtime/services/drain-merge-git-service.js +10 -0
- package/dist/runtime/services/git-porcelain-service.js +38 -0
- package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
- package/dist/runtime/services/kubernetes-argo-service.js +81 -0
- package/dist/runtime/services/mcp-gateway-service.js +184 -0
- package/dist/runtime/services/opencode-sdk-service.js +27 -0
- package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
- package/dist/runtime/services/select-candidate-service.js +13 -0
- package/dist/runtime/services/worktree-service.js +18 -0
- package/dist/schedule/passes/candidates.js +17 -8
- package/docs/config-architecture.md +105 -0
- package/package.json +7 -2
package/dist/config/defaults.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { parseDocument } from "yaml";
|
|
1
|
+
import { ConfigIoService, parseConfigYamlAs, runConfigIoSync } from "../runtime/services/config-io-service.js";
|
|
3
2
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
3
|
+
import { Effect } from "effect";
|
|
5
4
|
//#region src/config/defaults.ts
|
|
6
5
|
const PIPELINE_CONFIG_PATH = ".pipeline/pipeline.yaml";
|
|
7
6
|
const RUNNERS_CONFIG_PATH = ".pipeline/runners.yaml";
|
|
@@ -9,7 +8,9 @@ const PROFILES_CONFIG_PATH = ".pipeline/profiles.yaml";
|
|
|
9
8
|
const OPENCODE_ECOSYSTEM_MANIFEST_PATH = "defaults/opencode-ecosystem.yaml";
|
|
10
9
|
const DEFAULT_PACKAGE_DEFAULTS_ROOT = new URL("../../defaults/", import.meta.url);
|
|
11
10
|
function loadDefaultYaml(filename) {
|
|
12
|
-
return
|
|
11
|
+
return runConfigIoSync(Effect.gen(function* () {
|
|
12
|
+
return yield* (yield* ConfigIoService).readText(new URL(filename, DEFAULT_PACKAGE_DEFAULTS_ROOT));
|
|
13
|
+
}));
|
|
13
14
|
}
|
|
14
15
|
const PACKAGE_DEFAULT_RUNNERS_YAML = loadDefaultYaml("runners.yaml");
|
|
15
16
|
const PACKAGE_DEFAULT_PROFILES_YAML = loadDefaultYaml("profiles.yaml");
|
|
@@ -111,24 +112,13 @@ const openCodeEcosystemManifestSchema = z.object({
|
|
|
111
112
|
version: z.literal(1)
|
|
112
113
|
}).strict();
|
|
113
114
|
function parseOpenCodeEcosystemManifest(source, sourcePath = OPENCODE_ECOSYSTEM_MANIFEST_PATH) {
|
|
114
|
-
return
|
|
115
|
+
return runConfigIoSync(parseConfigYamlAs(source, sourcePath, openCodeEcosystemManifestSchema));
|
|
115
116
|
}
|
|
116
117
|
function loadDefaultOpenCodeEcosystemManifest() {
|
|
117
|
-
return
|
|
118
|
+
return runConfigIoSync(Effect.gen(function* () {
|
|
119
|
+
return yield* parseConfigYamlAs(yield* (yield* ConfigIoService).readText(DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST_URL), OPENCODE_ECOSYSTEM_MANIFEST_PATH, openCodeEcosystemManifestSchema);
|
|
120
|
+
}));
|
|
118
121
|
}
|
|
119
122
|
const DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST = loadDefaultOpenCodeEcosystemManifest();
|
|
120
|
-
function parseYamlAs(source, sourcePath, schema) {
|
|
121
|
-
const document = parseDocument(source, {
|
|
122
|
-
prettyErrors: false,
|
|
123
|
-
uniqueKeys: true
|
|
124
|
-
});
|
|
125
|
-
if (document.errors.length > 0) throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, document.errors.map((err) => ({
|
|
126
|
-
message: err.message,
|
|
127
|
-
path: sourcePath
|
|
128
|
-
})));
|
|
129
|
-
const parsed = schema.safeParse(document.toJS());
|
|
130
|
-
if (!parsed.success) throw validationError(configIssuesFromZodError(parsed.error));
|
|
131
|
-
return parsed.data;
|
|
132
|
-
}
|
|
133
123
|
//#endregion
|
|
134
124
|
export { DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST, OPENCODE_ECOSYSTEM_MANIFEST_PATH, PACKAGE_DEFAULT_PIPELINE_YAML, PACKAGE_DEFAULT_PROFILES_YAML, PACKAGE_DEFAULT_RUNNERS_YAML, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, RUNNERS_CONFIG_PATH, parseOpenCodeEcosystemManifest };
|
package/dist/config/load.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pipelineFileSchema, profilesFileSchema, runnersFileSchema } from "./schemas.js";
|
|
2
|
+
import { parseConfigYamlAs, runConfigIoSync } from "../runtime/services/config-io-service.js";
|
|
2
3
|
import { PACKAGE_DEFAULT_PIPELINE_YAML, PACKAGE_DEFAULT_PROFILES_YAML, PACKAGE_DEFAULT_RUNNERS_YAML, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, RUNNERS_CONFIG_PATH } from "./defaults.js";
|
|
3
4
|
import { validatePipelineConfig } from "./validate.js";
|
|
4
|
-
import {
|
|
5
|
+
import { Effect } from "effect";
|
|
5
6
|
//#region src/config/load.ts
|
|
6
7
|
function loadPipelineConfig(projectRoot, options = {}) {
|
|
7
8
|
return loadPackagePipelineConfig(projectRoot, options);
|
|
8
9
|
}
|
|
9
10
|
function loadPackagePipelineConfig(projectRoot, options = {}) {
|
|
10
|
-
return
|
|
11
|
+
return runConfigIoSync(parsePipelineConfigPartsEffect({
|
|
11
12
|
pipeline: PACKAGE_DEFAULT_PIPELINE_YAML,
|
|
12
13
|
profiles: PACKAGE_DEFAULT_PROFILES_YAML,
|
|
13
14
|
runners: PACKAGE_DEFAULT_RUNNERS_YAML
|
|
@@ -15,7 +16,7 @@ function loadPackagePipelineConfig(projectRoot, options = {}) {
|
|
|
15
16
|
pipeline: "@oisincoveney/pipeline/defaults/pipeline.yaml",
|
|
16
17
|
profiles: "@oisincoveney/pipeline/defaults/profiles.yaml",
|
|
17
18
|
runners: "@oisincoveney/pipeline/defaults/runners.yaml"
|
|
18
|
-
}, options);
|
|
19
|
+
}, options));
|
|
19
20
|
}
|
|
20
21
|
function parsePipelineConfigYaml(source, sourcePath = PIPELINE_CONFIG_PATH, projectRoot) {
|
|
21
22
|
return parsePipelineConfigParts({
|
|
@@ -28,46 +29,55 @@ function parsePipelineConfigYaml(source, sourcePath = PIPELINE_CONFIG_PATH, proj
|
|
|
28
29
|
runners: RUNNERS_CONFIG_PATH
|
|
29
30
|
});
|
|
30
31
|
}
|
|
32
|
+
function durabilityField(durability) {
|
|
33
|
+
return durability ? { durability } : {};
|
|
34
|
+
}
|
|
35
|
+
function pipe83Fields(pipeline) {
|
|
36
|
+
const keys = [
|
|
37
|
+
"best_of_n",
|
|
38
|
+
"context_handoff",
|
|
39
|
+
"parallel_worktrees",
|
|
40
|
+
"repo_map"
|
|
41
|
+
];
|
|
42
|
+
const source = pipeline;
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const key of keys) if (source[key] !== void 0) out[key] = source[key];
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
31
47
|
function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
|
|
32
48
|
pipeline: PIPELINE_CONFIG_PATH,
|
|
33
49
|
profiles: PROFILES_CONFIG_PATH,
|
|
34
50
|
runners: RUNNERS_CONFIG_PATH
|
|
35
51
|
}, options = {}) {
|
|
36
|
-
|
|
37
|
-
const profiles = parseYamlAs(sources.profiles, sourcePaths.profiles, profilesFileSchema);
|
|
38
|
-
const pipeline = parseYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
|
|
39
|
-
return validatePipelineConfig({
|
|
40
|
-
default_workflow: pipeline.default_workflow,
|
|
41
|
-
entrypoints: pipeline.entrypoints,
|
|
42
|
-
hooks: pipeline.hooks,
|
|
43
|
-
...profiles.mcp_gateway ? { mcp_gateway: profiles.mcp_gateway } : {},
|
|
44
|
-
mcp_servers: profiles.mcp_servers,
|
|
45
|
-
...pipeline.orchestrator ? { orchestrator: pipeline.orchestrator } : {},
|
|
46
|
-
profiles: profiles.profiles,
|
|
47
|
-
runner_command: pipeline.runner_command,
|
|
48
|
-
rules: profiles.rules,
|
|
49
|
-
runners: runners.runners,
|
|
50
|
-
scheduler: pipeline.scheduler,
|
|
51
|
-
schedules: pipeline.schedules,
|
|
52
|
-
skills: profiles.skills,
|
|
53
|
-
...pipeline.task_context ? { task_context: pipeline.task_context } : {},
|
|
54
|
-
token_budget: pipeline.token_budget,
|
|
55
|
-
version: 1,
|
|
56
|
-
workflows: pipeline.workflows
|
|
57
|
-
}, projectRoot, options);
|
|
52
|
+
return runConfigIoSync(parsePipelineConfigPartsEffect(sources, projectRoot, sourcePaths, options));
|
|
58
53
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
function parsePipelineConfigPartsEffect(sources, projectRoot, sourcePaths, options) {
|
|
55
|
+
return Effect.gen(function* () {
|
|
56
|
+
const runners = yield* parseConfigYamlAs(sources.runners, sourcePaths.runners, runnersFileSchema);
|
|
57
|
+
const profiles = yield* parseConfigYamlAs(sources.profiles, sourcePaths.profiles, profilesFileSchema);
|
|
58
|
+
const pipeline = yield* parseConfigYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
|
|
59
|
+
return validatePipelineConfig({
|
|
60
|
+
default_workflow: pipeline.default_workflow,
|
|
61
|
+
...durabilityField(pipeline.durability),
|
|
62
|
+
...pipe83Fields(pipeline),
|
|
63
|
+
entrypoints: pipeline.entrypoints,
|
|
64
|
+
hooks: pipeline.hooks,
|
|
65
|
+
...profiles.mcp_gateway ? { mcp_gateway: profiles.mcp_gateway } : {},
|
|
66
|
+
mcp_servers: profiles.mcp_servers,
|
|
67
|
+
...pipeline.orchestrator ? { orchestrator: pipeline.orchestrator } : {},
|
|
68
|
+
profiles: profiles.profiles,
|
|
69
|
+
runner_command: pipeline.runner_command,
|
|
70
|
+
rules: profiles.rules,
|
|
71
|
+
runners: runners.runners,
|
|
72
|
+
scheduler: pipeline.scheduler,
|
|
73
|
+
schedules: pipeline.schedules,
|
|
74
|
+
skills: profiles.skills,
|
|
75
|
+
...pipeline.task_context ? { task_context: pipeline.task_context } : {},
|
|
76
|
+
token_budget: pipeline.token_budget,
|
|
77
|
+
version: 1,
|
|
78
|
+
workflows: pipeline.workflows
|
|
79
|
+
}, projectRoot, options);
|
|
63
80
|
});
|
|
64
|
-
if (document.errors.length > 0) throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, document.errors.map((err) => ({
|
|
65
|
-
message: err.message,
|
|
66
|
-
path: sourcePath
|
|
67
|
-
})));
|
|
68
|
-
const parsed = schema.safeParse(document.toJS());
|
|
69
|
-
if (!parsed.success) throw validationError(configIssuesFromZodError(parsed.error));
|
|
70
|
-
return parsed.data;
|
|
71
81
|
}
|
|
72
82
|
//#endregion
|
|
73
83
|
export { loadPackagePipelineConfig, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml };
|
package/dist/config/schemas.d.ts
CHANGED
|
@@ -14,9 +14,14 @@ interface PipelineConfigIssue {
|
|
|
14
14
|
message: string;
|
|
15
15
|
path?: string;
|
|
16
16
|
}
|
|
17
|
-
declare
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
declare const PipelineConfigError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
18
|
+
readonly _tag: "PipelineConfigError";
|
|
19
|
+
} & Readonly<A>;
|
|
20
|
+
declare class PipelineConfigError extends PipelineConfigError_base<{
|
|
21
|
+
readonly code: PipelineConfigErrorCode;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly issues: PipelineConfigIssue[];
|
|
24
|
+
}> {
|
|
20
25
|
constructor(code: PipelineConfigErrorCode, message: string, issues?: PipelineConfigIssue[]);
|
|
21
26
|
}
|
|
22
27
|
declare const workflowNodeBaseSchema: z.ZodObject<{
|
|
@@ -221,8 +226,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
221
226
|
policy: z.ZodOptional<z.ZodObject<{
|
|
222
227
|
commands: z.ZodOptional<z.ZodEnum<{
|
|
223
228
|
allow: "allow";
|
|
224
|
-
deny: "deny";
|
|
225
229
|
"trusted-only": "trusted-only";
|
|
230
|
+
deny: "deny";
|
|
226
231
|
}>>;
|
|
227
232
|
modules: z.ZodOptional<z.ZodEnum<{
|
|
228
233
|
allow: "allow";
|
|
@@ -245,9 +250,13 @@ declare const configSchema: z.ZodObject<{
|
|
|
245
250
|
}>>;
|
|
246
251
|
}, z.core.$strict>>>;
|
|
247
252
|
default_profile: z.ZodOptional<z.ZodString>;
|
|
253
|
+
host_scope: z.ZodDefault<z.ZodEnum<{
|
|
254
|
+
project: "project";
|
|
255
|
+
global: "global";
|
|
256
|
+
}>>;
|
|
248
257
|
mode: z.ZodEnum<{
|
|
249
|
-
local: "local";
|
|
250
258
|
hosted: "hosted";
|
|
259
|
+
local: "local";
|
|
251
260
|
}>;
|
|
252
261
|
provider: z.ZodLiteral<"toolhive">;
|
|
253
262
|
authorization_env: z.ZodDefault<z.ZodString>;
|
|
@@ -290,10 +299,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
290
299
|
}, z.core.$strict>>;
|
|
291
300
|
output: z.ZodOptional<z.ZodObject<{
|
|
292
301
|
format: z.ZodEnum<{
|
|
293
|
-
json_schema: "json_schema";
|
|
294
302
|
text: "text";
|
|
295
303
|
json: "json";
|
|
296
304
|
jsonl: "jsonl";
|
|
305
|
+
json_schema: "json_schema";
|
|
297
306
|
}>;
|
|
298
307
|
repair: z.ZodOptional<z.ZodObject<{
|
|
299
308
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -362,10 +371,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
362
371
|
disabled: "disabled";
|
|
363
372
|
}>>>;
|
|
364
373
|
output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
365
|
-
json_schema: "json_schema";
|
|
366
374
|
text: "text";
|
|
367
375
|
json: "json";
|
|
368
376
|
jsonl: "jsonl";
|
|
377
|
+
json_schema: "json_schema";
|
|
369
378
|
}>>>;
|
|
370
379
|
rules: z.ZodOptional<z.ZodBoolean>;
|
|
371
380
|
skills: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -502,9 +511,17 @@ declare const configSchema: z.ZodObject<{
|
|
|
502
511
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
503
512
|
model: z.ZodOptional<z.ZodString>;
|
|
504
513
|
}, z.core.$strict>>;
|
|
514
|
+
durability: z.ZodOptional<z.ZodObject<{
|
|
515
|
+
dir: z.ZodDefault<z.ZodString>;
|
|
516
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
517
|
+
}, z.core.$strict>>;
|
|
505
518
|
parallel_worktrees: z.ZodOptional<z.ZodObject<{
|
|
506
519
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
507
520
|
}, z.core.$strict>>;
|
|
521
|
+
repo_map: z.ZodOptional<z.ZodObject<{
|
|
522
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
523
|
+
token_budget: z.ZodDefault<z.ZodNumber>;
|
|
524
|
+
}, z.core.$strict>>;
|
|
508
525
|
token_budget: z.ZodDefault<z.ZodObject<{
|
|
509
526
|
default_context_window: z.ZodDefault<z.ZodNumber>;
|
|
510
527
|
max_context_pct: z.ZodDefault<z.ZodNumber>;
|
package/dist/config/schemas.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { Data } from "effect";
|
|
2
3
|
//#region src/config/schemas.ts
|
|
3
4
|
const ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
4
5
|
const RUNNER_TYPES = ["opencode", "command"];
|
|
@@ -58,14 +59,13 @@ const DEFAULT_RUNNER_COMMAND_GIT_COMMITTER = {
|
|
|
58
59
|
email: "git@oisin.ee",
|
|
59
60
|
name: "oisin-bot"
|
|
60
61
|
};
|
|
61
|
-
var PipelineConfigError = class extends
|
|
62
|
-
code;
|
|
63
|
-
issues;
|
|
62
|
+
var PipelineConfigError = class extends Data.TaggedError("PipelineConfigError") {
|
|
64
63
|
constructor(code, message, issues = []) {
|
|
65
|
-
super(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
super({
|
|
65
|
+
code,
|
|
66
|
+
message,
|
|
67
|
+
issues
|
|
68
|
+
});
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
const strictRecord = (valueSchema) => z.record(z.string(), valueSchema);
|
|
@@ -155,6 +155,7 @@ const mcpGatewayBackendSchema = z.object({
|
|
|
155
155
|
const mcpGatewaySchema = z.object({
|
|
156
156
|
backends: strictRecord(mcpGatewayBackendSchema).default({}),
|
|
157
157
|
default_profile: z.string().min(1).optional(),
|
|
158
|
+
host_scope: z.enum(["project", "global"]).default("project"),
|
|
158
159
|
mode: z.enum(["hosted", "local"]),
|
|
159
160
|
provider: z.literal("toolhive"),
|
|
160
161
|
authorization_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_AUTHORIZATION"),
|
|
@@ -466,12 +467,20 @@ const contextHandoffSchema = z.object({
|
|
|
466
467
|
model: z.string().optional()
|
|
467
468
|
}).strict();
|
|
468
469
|
const parallelWorktreesSchema = z.object({ enabled: z.boolean().default(false) }).strict();
|
|
470
|
+
const durabilitySchema = z.object({
|
|
471
|
+
dir: z.string().min(1).default(".pipeline/journal"),
|
|
472
|
+
enabled: z.boolean().default(false)
|
|
473
|
+
}).strict();
|
|
469
474
|
const bestOfNSchema = z.object({
|
|
470
475
|
categories: z.array(z.string()).default(["green"]),
|
|
471
476
|
enabled: z.boolean().default(false),
|
|
472
477
|
judge_model: z.string().optional(),
|
|
473
478
|
n: z.number().int().positive().default(1)
|
|
474
479
|
}).strict();
|
|
480
|
+
const repoMapSchema = z.object({
|
|
481
|
+
enabled: z.boolean().default(false),
|
|
482
|
+
token_budget: z.number().int().positive().default(2e3)
|
|
483
|
+
}).strict();
|
|
475
484
|
const pipelineFileSchema = z.object({
|
|
476
485
|
default_workflow: z.string(),
|
|
477
486
|
entrypoints: strictRecord(entrypointSchema).default({}),
|
|
@@ -495,7 +504,9 @@ const pipelineFileSchema = z.object({
|
|
|
495
504
|
task_context: taskContextResolverSchema.optional(),
|
|
496
505
|
best_of_n: bestOfNSchema.optional(),
|
|
497
506
|
context_handoff: contextHandoffSchema.optional(),
|
|
507
|
+
durability: durabilitySchema.optional(),
|
|
498
508
|
parallel_worktrees: parallelWorktreesSchema.optional(),
|
|
509
|
+
repo_map: repoMapSchema.optional(),
|
|
499
510
|
token_budget: tokenBudgetSchema.default(DEFAULT_TOKEN_BUDGET),
|
|
500
511
|
workflows: strictRecord(workflowSchema).default({}),
|
|
501
512
|
version: z.literal(1)
|
|
@@ -529,7 +540,9 @@ const configSchema = z.object({
|
|
|
529
540
|
task_context: taskContextResolverSchema.optional(),
|
|
530
541
|
best_of_n: bestOfNSchema.optional(),
|
|
531
542
|
context_handoff: contextHandoffSchema.optional(),
|
|
543
|
+
durability: durabilitySchema.optional(),
|
|
532
544
|
parallel_worktrees: parallelWorktreesSchema.optional(),
|
|
545
|
+
repo_map: repoMapSchema.optional(),
|
|
533
546
|
token_budget: tokenBudgetSchema.default(DEFAULT_TOKEN_BUDGET),
|
|
534
547
|
version: z.literal(1),
|
|
535
548
|
workflows: strictRecord(workflowSchema).default({})
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { estimateTokens } from "../token-estimator.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { extname, join, relative } from "node:path";
|
|
5
|
+
import Graph from "graphology";
|
|
6
|
+
import pagerank from "graphology-metrics/centrality/pagerank.js";
|
|
7
|
+
import { Language, Parser, Query } from "web-tree-sitter";
|
|
8
|
+
//#region src/context/repo-map.ts
|
|
9
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
10
|
+
".cjs",
|
|
11
|
+
".js",
|
|
12
|
+
".jsx",
|
|
13
|
+
".mjs",
|
|
14
|
+
".ts",
|
|
15
|
+
".tsx"
|
|
16
|
+
]);
|
|
17
|
+
const SKIP_DIRS = new Set([
|
|
18
|
+
"node_modules",
|
|
19
|
+
".git",
|
|
20
|
+
"dist",
|
|
21
|
+
".pipeline"
|
|
22
|
+
]);
|
|
23
|
+
const SEED_BONUS = 1;
|
|
24
|
+
const WORD_RE = /[a-z_][a-z0-9_]+/gi;
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
let parserPromise = null;
|
|
27
|
+
const languageCache = /* @__PURE__ */ new Map();
|
|
28
|
+
const queryCache = /* @__PURE__ */ new Map();
|
|
29
|
+
function getParser() {
|
|
30
|
+
parserPromise ??= Parser.init().then(() => new Parser());
|
|
31
|
+
return parserPromise;
|
|
32
|
+
}
|
|
33
|
+
function loadLanguage(grammar) {
|
|
34
|
+
const cached = languageCache.get(grammar);
|
|
35
|
+
if (cached) return cached;
|
|
36
|
+
const promise = Language.load(require.resolve(`tree-sitter-${grammar}/tree-sitter-${grammar}.wasm`));
|
|
37
|
+
languageCache.set(grammar, promise);
|
|
38
|
+
return promise;
|
|
39
|
+
}
|
|
40
|
+
function tagsQuery(language, grammar) {
|
|
41
|
+
const cached = queryCache.get(grammar);
|
|
42
|
+
if (cached) return cached;
|
|
43
|
+
const query = new Query(language, `${readFileSync(require.resolve("tree-sitter-javascript/queries/tags.scm"), "utf8")}\n${readFileSync(require.resolve("tree-sitter-typescript/queries/tags.scm"), "utf8")}`);
|
|
44
|
+
queryCache.set(grammar, query);
|
|
45
|
+
return query;
|
|
46
|
+
}
|
|
47
|
+
function grammarFor(file) {
|
|
48
|
+
const ext = extname(file);
|
|
49
|
+
return ext === ".ts" || ext === ".tsx" ? "typescript" : "javascript";
|
|
50
|
+
}
|
|
51
|
+
function discoverFiles(root) {
|
|
52
|
+
const found = [];
|
|
53
|
+
walkDir(root, found);
|
|
54
|
+
return found.sort();
|
|
55
|
+
}
|
|
56
|
+
function walkDir(dir, found) {
|
|
57
|
+
for (const entry of readdirSync(dir).sort()) handleEntry(join(dir, entry), entry, found);
|
|
58
|
+
}
|
|
59
|
+
function handleEntry(full, name, found) {
|
|
60
|
+
if (SKIP_DIRS.has(name)) return;
|
|
61
|
+
if (statSync(full).isDirectory()) {
|
|
62
|
+
walkDir(full, found);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (SOURCE_EXTENSIONS.has(extname(name))) found.push(full);
|
|
66
|
+
}
|
|
67
|
+
async function tagFile(root, file) {
|
|
68
|
+
const parser = await getParser();
|
|
69
|
+
const grammar = grammarFor(file);
|
|
70
|
+
const language = await loadLanguage(grammar);
|
|
71
|
+
parser.setLanguage(language);
|
|
72
|
+
const path = relative(root, file);
|
|
73
|
+
const tree = parser.parse(readFileSync(file, "utf8"));
|
|
74
|
+
const tags = {
|
|
75
|
+
definitions: [],
|
|
76
|
+
path,
|
|
77
|
+
references: []
|
|
78
|
+
};
|
|
79
|
+
if (!tree) return tags;
|
|
80
|
+
for (const match of tagsQuery(language, grammar).matches(tree.rootNode)) addMatch(tags, path, match);
|
|
81
|
+
return tags;
|
|
82
|
+
}
|
|
83
|
+
function addMatch(tags, path, match) {
|
|
84
|
+
const nameCapture = match.captures.find((c) => c.name === "name");
|
|
85
|
+
if (!nameCapture) return;
|
|
86
|
+
const name = nameCapture.node.text;
|
|
87
|
+
const def = match.captures.find((c) => c.name.startsWith("definition."));
|
|
88
|
+
if (def) {
|
|
89
|
+
tags.definitions.push({
|
|
90
|
+
endLine: def.node.endPosition.row + 1,
|
|
91
|
+
kind: def.name.slice(11),
|
|
92
|
+
name,
|
|
93
|
+
path,
|
|
94
|
+
startLine: def.node.startPosition.row + 1
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (match.captures.some((c) => c.name.startsWith("reference."))) tags.references.push(name);
|
|
99
|
+
}
|
|
100
|
+
function definitionKey(def) {
|
|
101
|
+
return `def:${def.path}#${def.name}#${def.startLine}`;
|
|
102
|
+
}
|
|
103
|
+
function isSeeded(def, seedNames, artifacts) {
|
|
104
|
+
if (seedNames.has(def.name.toLowerCase())) return true;
|
|
105
|
+
return artifacts.some((artifact) => artifact.path === def.path && (!artifact.lineRange || def.startLine <= artifact.lineRange[1] && def.endLine >= artifact.lineRange[0]));
|
|
106
|
+
}
|
|
107
|
+
function buildGraph(fileTags, input) {
|
|
108
|
+
const seedNames = new Set(input.taskText.toLowerCase().match(WORD_RE) ?? []);
|
|
109
|
+
const graph = new Graph({
|
|
110
|
+
allowSelfLoops: false,
|
|
111
|
+
type: "directed"
|
|
112
|
+
});
|
|
113
|
+
const defsByName = /* @__PURE__ */ new Map();
|
|
114
|
+
for (const file of fileTags) for (const def of file.definitions) addDefNode(graph, defsByName, def, isSeeded(def, seedNames, input.artifacts));
|
|
115
|
+
linkReferences(graph, fileTags, defsByName);
|
|
116
|
+
return graph;
|
|
117
|
+
}
|
|
118
|
+
function addDefNode(graph, defsByName, def, matchedSeed) {
|
|
119
|
+
const key = definitionKey(def);
|
|
120
|
+
graph.mergeNode(key, {
|
|
121
|
+
def,
|
|
122
|
+
matchedSeed
|
|
123
|
+
});
|
|
124
|
+
defsByName.set(def.name, [...defsByName.get(def.name) ?? [], key]);
|
|
125
|
+
}
|
|
126
|
+
function linkReferences(graph, fileTags, defsByName) {
|
|
127
|
+
for (const file of fileTags) {
|
|
128
|
+
const fileKey = `file:${file.path}`;
|
|
129
|
+
graph.mergeNode(fileKey);
|
|
130
|
+
linkFileReferences(graph, fileKey, file.references, defsByName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function linkFileReferences(graph, fileKey, references, defsByName) {
|
|
134
|
+
for (const name of references) for (const target of defsByName.get(name) ?? []) graph.mergeEdge(fileKey, target, { weight: 1 });
|
|
135
|
+
}
|
|
136
|
+
function pagerankScores(graph) {
|
|
137
|
+
try {
|
|
138
|
+
return pagerank(graph, {
|
|
139
|
+
getEdgeWeight: "weight",
|
|
140
|
+
maxIterations: 200,
|
|
141
|
+
tolerance: 1e-4
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function rankDefinitions(graph) {
|
|
148
|
+
const scores = pagerankScores(graph);
|
|
149
|
+
const ranked = [];
|
|
150
|
+
graph.forEachNode((key, attrs) => {
|
|
151
|
+
if (key.startsWith("def:")) ranked.push(toSymbol(attrs.def, Boolean(attrs.matchedSeed), scores[key] ?? 0));
|
|
152
|
+
});
|
|
153
|
+
return ranked.sort(compareSymbols);
|
|
154
|
+
}
|
|
155
|
+
function toSymbol(def, matchedSeed, pageRankScore) {
|
|
156
|
+
return {
|
|
157
|
+
kind: def.kind,
|
|
158
|
+
lineRange: [def.startLine, def.endLine],
|
|
159
|
+
matchedSeed,
|
|
160
|
+
name: def.name,
|
|
161
|
+
path: def.path,
|
|
162
|
+
score: pageRankScore + (matchedSeed ? SEED_BONUS : 0)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function compareSymbols(a, b) {
|
|
166
|
+
return b.score - a.score || a.path.localeCompare(b.path) || a.name.localeCompare(b.name) || a.lineRange[0] - b.lineRange[0];
|
|
167
|
+
}
|
|
168
|
+
function renderContext(selected) {
|
|
169
|
+
return ["Repo map context:", ...selected.map((s) => `## ${s.path}:${s.lineRange[0]}-${s.lineRange[1]}\n${s.kind} ${s.name}`)].join("\n");
|
|
170
|
+
}
|
|
171
|
+
function selectWithinBudget(ranked, budget, estimateTokens) {
|
|
172
|
+
let low = 0;
|
|
173
|
+
let high = ranked.length;
|
|
174
|
+
let best = 0;
|
|
175
|
+
while (low <= high) {
|
|
176
|
+
const mid = Math.floor((low + high) / 2);
|
|
177
|
+
if (estimateTokens(renderContext(ranked.slice(0, mid))) <= budget) {
|
|
178
|
+
best = mid;
|
|
179
|
+
low = mid + 1;
|
|
180
|
+
} else high = mid - 1;
|
|
181
|
+
}
|
|
182
|
+
const selected = ranked.slice(0, best);
|
|
183
|
+
const context = renderContext(selected);
|
|
184
|
+
return {
|
|
185
|
+
context,
|
|
186
|
+
estimatedTokens: estimateTokens(context),
|
|
187
|
+
selected
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function buildRepoMapContext(input) {
|
|
191
|
+
const estimateTokens$1 = input.estimateTokens ?? estimateTokens;
|
|
192
|
+
const ranked = rankDefinitions(buildGraph(await Promise.all(discoverFiles(input.worktreePath).map((file) => tagFile(input.worktreePath, file))), input));
|
|
193
|
+
const { context, estimatedTokens, selected } = selectWithinBudget(ranked, input.tokenBudget, estimateTokens$1);
|
|
194
|
+
return {
|
|
195
|
+
budget: input.tokenBudget,
|
|
196
|
+
context,
|
|
197
|
+
estimatedTokens,
|
|
198
|
+
selected,
|
|
199
|
+
totalRanked: ranked.length
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
export { buildRepoMapContext };
|
|
@@ -257,9 +257,18 @@ function namedOpencodePermissionMap(names) {
|
|
|
257
257
|
function toolPermission(allowed, tool) {
|
|
258
258
|
return allowed.has(tool) ? "allow" : "deny";
|
|
259
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* PIPE-83.11: whether to synthesize the singleton pipeline gateway into this
|
|
262
|
+
* repo's `.opencode/opencode.json`. A "global"-scoped gateway is registered
|
|
263
|
+
* once in the global opencode config (via `moka gateway configure-host
|
|
264
|
+
* --scope global`) and inherited, so it is not embedded per project.
|
|
265
|
+
*/
|
|
266
|
+
function shouldEmbedProjectGateway(config) {
|
|
267
|
+
return config.mcp_gateway !== void 0 && config.mcp_gateway.host_scope !== "global";
|
|
268
|
+
}
|
|
260
269
|
function renderOpenCodeProjectConfig(config) {
|
|
261
270
|
return formatOpenCodeProjectJson({
|
|
262
|
-
...config
|
|
271
|
+
...shouldEmbedProjectGateway(config) ? JSON.parse(renderOpenCodeGatewayConfig(config)) : { $schema: "https://opencode.ai/config.json" },
|
|
263
272
|
lsp: true,
|
|
264
273
|
...opencodePluginConfig(),
|
|
265
274
|
...opencodeProviderConfig()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
//#region src/mcp/gateway-error.ts
|
|
3
|
+
/**
|
|
4
|
+
* Tagged error for the MCP (ToolHive) gateway subsystem. Lives in its own module
|
|
5
|
+
* so both the gateway facade (src/mcp/gateway.ts) and the Effect service
|
|
6
|
+
* (src/runtime/services/mcp-gateway-service.ts) can import it without forming a
|
|
7
|
+
* circular dependency between them.
|
|
8
|
+
*/
|
|
9
|
+
var PipelineMcpGatewayError = class extends Data.TaggedError("PipelineMcpGatewayError") {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super({ message });
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { PipelineMcpGatewayError };
|