@oisincoveney/pipeline 2.6.0 → 2.8.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/defaults/pipeline.yaml +25 -0
- package/dist/argo-graph.js +7 -7
- package/dist/bench/eval-report.js +27 -0
- package/dist/cli/program.js +19 -3
- package/dist/commands/bench-command.js +18 -0
- package/dist/config/load.js +17 -0
- package/dist/config/schemas.d.ts +22 -5
- 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.js +3 -3
- package/dist/moka-submit.d.ts +6 -6
- package/dist/pipeline-init.js +18 -12
- package/dist/pipeline-runtime.js +12 -1
- 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/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 +6 -5
- package/dist/runner.d.ts +6 -1
- package/dist/runner.js +3 -3
- package/dist/runtime/agent-node/agent-node.js +22 -4
- package/dist/runtime/local-scheduler.js +45 -0
- package/dist/runtime/opencode-server.js +6 -3
- package/dist/runtime/parallel-node/parallel-node.js +77 -58
- 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 +13 -1
- 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/defaults/pipeline.yaml
CHANGED
|
@@ -2,6 +2,31 @@ version: 1
|
|
|
2
2
|
default_workflow: inspect
|
|
3
3
|
orchestrator:
|
|
4
4
|
profile: moka-orchestrator
|
|
5
|
+
# PIPE-83 architecture hardening — ON by default so moka actually uses it.
|
|
6
|
+
# context_handoff: nodes pass curated NodeHandoffs downstream instead of raw
|
|
7
|
+
# transitive transcripts (kills the re-hydration latency/quality leak).
|
|
8
|
+
context_handoff:
|
|
9
|
+
enabled: true
|
|
10
|
+
# repo_map: prepend a tree-sitter + PageRank ranked code map to agent prompts,
|
|
11
|
+
# seeded by the node's task + handoff artifacts, within token_budget.
|
|
12
|
+
repo_map:
|
|
13
|
+
enabled: true
|
|
14
|
+
# durability: journal each terminal node result so a killed run resumes from the
|
|
15
|
+
# last passed node without re-running (or re-spending tokens on) finished work.
|
|
16
|
+
durability:
|
|
17
|
+
enabled: true
|
|
18
|
+
# best_of_n / parallel_worktrees: the verifier-pattern dial. Schedule generation
|
|
19
|
+
# + selection are validated/tested, BUT a live end-to-end run (2026-06-16) proved
|
|
20
|
+
# the EXECUTION is not yet production-ready, so it stays OFF by default (on-by-
|
|
21
|
+
# default made real runs hang). Two runtime gaps remain — see PIPE-83.14:
|
|
22
|
+
# 1. The leased opencode server is rooted at the main worktree and throws
|
|
23
|
+
# "Unexpected server error" when a candidate session runs with directory set
|
|
24
|
+
# to its isolated worktree → candidates exit 70, retries exhaust, green loops.
|
|
25
|
+
# Fix: lease a per-worktree opencode server for each candidate.
|
|
26
|
+
# 2. The winning candidate's file changes live in its worktree and are never
|
|
27
|
+
# merged back to the main tree, so downstream nodes wouldn't see them.
|
|
28
|
+
# Enable explicitly (best_of_n.enabled + n:2 + categories:[green] + parallel_
|
|
29
|
+
# worktrees.enabled) once those land; n=2 also ~doubles green-node spend.
|
|
5
30
|
token_budget:
|
|
6
31
|
default_context_window: 200000
|
|
7
32
|
max_context_pct: 50
|
package/dist/argo-graph.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { uniqueStrings } from "./strings.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Data } from "effect";
|
|
3
4
|
//#region src/argo-graph.ts
|
|
4
5
|
const argoExecutableTaskSchema = z.object({
|
|
5
6
|
dependencies: z.array(z.string().min(1)),
|
|
@@ -18,14 +19,13 @@ const argoExecutionGraphSchema = z.object({
|
|
|
18
19
|
* lowered to an Argo DAG task. Callers should surface this as a validation
|
|
19
20
|
* failure before attempting a cluster submission.
|
|
20
21
|
*/
|
|
21
|
-
var ArgoGraphCompilerError = class extends
|
|
22
|
-
kind;
|
|
23
|
-
nodeId;
|
|
22
|
+
var ArgoGraphCompilerError = class extends Data.TaggedError("ArgoGraphCompilerError") {
|
|
24
23
|
constructor(kind, nodeId) {
|
|
25
|
-
super(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
super({
|
|
25
|
+
kind,
|
|
26
|
+
nodeId,
|
|
27
|
+
message: `Argo graph compiler: node kind '${kind}' on node '${nodeId}' cannot be lowered to an Argo DAG task`
|
|
28
|
+
});
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
function compileArgoExecutionGraph(plan) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/bench/eval-report.ts
|
|
2
|
+
function buildEvalReport(results) {
|
|
3
|
+
const variants = [...new Set(results.map((r) => r.variant))].sort();
|
|
4
|
+
return {
|
|
5
|
+
tasks: new Set(results.map((r) => r.task)).size,
|
|
6
|
+
variants: variants.map((variant) => summarizeVariant(variant, results.filter((r) => r.variant === variant)))
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function summarizeVariant(variant, runs) {
|
|
10
|
+
const resolved = runs.filter((r) => r.resolved).length;
|
|
11
|
+
const totalWall = runs.reduce((sum, r) => sum + r.wallMs, 0);
|
|
12
|
+
return {
|
|
13
|
+
avgWallMs: runs.length ? Math.round(totalWall / runs.length) : 0,
|
|
14
|
+
count: runs.length,
|
|
15
|
+
resolutionRate: runs.length ? resolved / runs.length : 0,
|
|
16
|
+
resolved,
|
|
17
|
+
totalCostTokens: runs.reduce((sum, r) => sum + r.costTokens, 0),
|
|
18
|
+
variant
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function renderEvalReport(report) {
|
|
22
|
+
const lines = [`Eval over ${report.tasks} task(s):`, "variant | resolved | rate | tokens | avg ms"];
|
|
23
|
+
for (const v of report.variants) lines.push(`${v.variant} | ${v.resolved}/${v.count} | ${(v.resolutionRate * 100).toFixed(0)}% | ${v.totalCostTokens} | ${v.avgWallMs}`);
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { buildEvalReport, renderEvalReport };
|
package/dist/cli/program.js
CHANGED
|
@@ -7,8 +7,11 @@ import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifac
|
|
|
7
7
|
import { loadMokaGlobalConfig } from "../moka-global-config.js";
|
|
8
8
|
import { defaultClusterDoctorNamespace, runClusterDoctor } from "../cluster-doctor.js";
|
|
9
9
|
import { formatCodexAuthSyncResult, syncLocalCodexAuth } from "../codex-auth-sync.js";
|
|
10
|
+
import { registerBenchCommand } from "../commands/bench-command.js";
|
|
10
11
|
import { registerConfiguredEntrypointCommands } from "../commands/pipeline-command.js";
|
|
11
12
|
import { configureGatewayHosts, localGatewayStatus, reconcileGateway, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "../mcp/gateway.js";
|
|
13
|
+
import { generateRuntimeRunId } from "../runtime/context/context.js";
|
|
14
|
+
import "../runtime/context/index.js";
|
|
12
15
|
import { runPipelineFromConfig } from "../pipeline-runtime.js";
|
|
13
16
|
import { registerRunnerCommandCommand } from "../commands/runner-command-command.js";
|
|
14
17
|
import { formatConfigLintWarning, lintPipelineConfig } from "../config/lint.js";
|
|
@@ -47,7 +50,14 @@ function quick(description, options = {}) {
|
|
|
47
50
|
entrypoint: "quick"
|
|
48
51
|
});
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
function withRunId(inputs) {
|
|
54
|
+
return {
|
|
55
|
+
...inputs,
|
|
56
|
+
runId: inputs.runId ?? generateRuntimeRunId()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function runConfiguredPipeline(rawInputs) {
|
|
60
|
+
const inputs = withRunId(rawInputs);
|
|
51
61
|
const config = loadPipelineConfig(inputs.worktreePath, { allowMissingLintFileReferences: true });
|
|
52
62
|
if (inputs.schedule) {
|
|
53
63
|
const compiled = compileScheduleArtifact(config, parseScheduleArtifact(readFileSync(inputs.schedule, "utf8"), inputs.schedule), inputs.worktreePath);
|
|
@@ -70,6 +80,7 @@ async function runConfiguredPipeline(inputs) {
|
|
|
70
80
|
const result = await generateScheduleArtifact({
|
|
71
81
|
config,
|
|
72
82
|
entrypointId: scheduledEntrypoint,
|
|
83
|
+
runId: inputs.runId,
|
|
73
84
|
task: inputs.task,
|
|
74
85
|
worktreePath: inputs.worktreePath
|
|
75
86
|
});
|
|
@@ -94,6 +105,7 @@ async function runAndPrintPipeline(inputs) {
|
|
|
94
105
|
config: inputs.config,
|
|
95
106
|
reporter,
|
|
96
107
|
entrypoint: inputs.entrypoint,
|
|
108
|
+
runId: inputs.runId,
|
|
97
109
|
task: inputs.task,
|
|
98
110
|
workflowId: inputs.workflow,
|
|
99
111
|
worktreePath: inputs.worktreePath
|
|
@@ -177,8 +189,11 @@ function createCliProgram() {
|
|
|
177
189
|
const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
|
|
178
190
|
console.log(await localGatewayStatus(cwd));
|
|
179
191
|
});
|
|
180
|
-
program.command("init").description("Initialize package-owned pipeline support without repo-local config").action(async () => {
|
|
181
|
-
const result = await initPipelineProject({
|
|
192
|
+
program.command("init").description("Initialize package-owned pipeline support without repo-local config").addOption(new Option("--skill-scope <scope>", "where to install default skills: project (repo-local copy) or personal (one inherited user/global install)").choices(["project", "personal"]).default("project")).action(async (flags) => {
|
|
193
|
+
const result = await initPipelineProject({
|
|
194
|
+
cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
|
|
195
|
+
scope: flags.skillScope
|
|
196
|
+
});
|
|
182
197
|
console.log(formatPipelineInitResult(result));
|
|
183
198
|
});
|
|
184
199
|
program.command("install-commands").description("Install generated slash-command adapters into this repository").addOption(new Option("--host <host>", "host command set to install").choices([
|
|
@@ -207,6 +222,7 @@ function createCliProgram() {
|
|
|
207
222
|
if (result.workflowUid) console.log(`Workflow UID: ${result.workflowUid}`);
|
|
208
223
|
});
|
|
209
224
|
registerRunnerCommandCommand(program);
|
|
225
|
+
registerBenchCommand(program);
|
|
210
226
|
const configuredEntrypointCommands = registerConfiguredEntrypointCommands(program, omitConfiguredEntrypoints(configuredPipeline, ["execute", "quick"]), async (entrypoint, task, _opts) => {
|
|
211
227
|
await execute(task, { entrypoint });
|
|
212
228
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buildEvalReport, renderEvalReport } from "../bench/eval-report.js";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
//#region src/commands/bench-command.ts
|
|
4
|
+
/**
|
|
5
|
+
* PIPE-83.6: `moka bench` — score a flat single-agent baseline vs the pipeline
|
|
6
|
+
* (and ablations) over a recorded run set. Runs are produced by executing the
|
|
7
|
+
* bench task set (bench/tasks) through `moka run` for each variant and recording
|
|
8
|
+
* one EvalRunResult per task+variant; this command turns those records into the
|
|
9
|
+
* comparison report.
|
|
10
|
+
*/
|
|
11
|
+
function registerBenchCommand(program) {
|
|
12
|
+
program.command("bench").description("Score a flat single-agent baseline vs the pipeline over recorded bench runs").requiredOption("--results <path>", "JSON file: array of { task, variant, resolved, costTokens, wallMs }").action((options) => {
|
|
13
|
+
const records = JSON.parse(readFileSync(options.results, "utf8"));
|
|
14
|
+
process.stdout.write(`${renderEvalReport(buildEvalReport(records))}\n`);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { registerBenchCommand };
|
package/dist/config/load.js
CHANGED
|
@@ -28,6 +28,21 @@ function parsePipelineConfigYaml(source, sourcePath = PIPELINE_CONFIG_PATH, proj
|
|
|
28
28
|
runners: RUNNERS_CONFIG_PATH
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
function durabilityField(durability) {
|
|
32
|
+
return durability ? { durability } : {};
|
|
33
|
+
}
|
|
34
|
+
function pipe83Fields(pipeline) {
|
|
35
|
+
const keys = [
|
|
36
|
+
"best_of_n",
|
|
37
|
+
"context_handoff",
|
|
38
|
+
"parallel_worktrees",
|
|
39
|
+
"repo_map"
|
|
40
|
+
];
|
|
41
|
+
const source = pipeline;
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const key of keys) if (source[key] !== void 0) out[key] = source[key];
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
31
46
|
function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
|
|
32
47
|
pipeline: PIPELINE_CONFIG_PATH,
|
|
33
48
|
profiles: PROFILES_CONFIG_PATH,
|
|
@@ -38,6 +53,8 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
|
|
|
38
53
|
const pipeline = parseYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
|
|
39
54
|
return validatePipelineConfig({
|
|
40
55
|
default_workflow: pipeline.default_workflow,
|
|
56
|
+
...durabilityField(pipeline.durability),
|
|
57
|
+
...pipe83Fields(pipeline),
|
|
41
58
|
entrypoints: pipeline.entrypoints,
|
|
42
59
|
hooks: pipeline.hooks,
|
|
43
60
|
...profiles.mcp_gateway ? { mcp_gateway: profiles.mcp_gateway } : {},
|
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<{
|
|
@@ -245,6 +250,10 @@ 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
258
|
local: "local";
|
|
250
259
|
hosted: "hosted";
|
|
@@ -344,8 +353,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
344
353
|
rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
345
354
|
path: z.ZodString;
|
|
346
355
|
source_root: z.ZodDefault<z.ZodEnum<{
|
|
347
|
-
package: "package";
|
|
348
356
|
project: "project";
|
|
357
|
+
package: "package";
|
|
349
358
|
}>>;
|
|
350
359
|
}, z.core.$strict>>>;
|
|
351
360
|
runners: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -485,8 +494,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
485
494
|
skills: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
486
495
|
path: z.ZodString;
|
|
487
496
|
source_root: z.ZodDefault<z.ZodEnum<{
|
|
488
|
-
package: "package";
|
|
489
497
|
project: "project";
|
|
498
|
+
package: "package";
|
|
490
499
|
}>>;
|
|
491
500
|
}, z.core.$strict>>>;
|
|
492
501
|
task_context: z.ZodOptional<z.ZodObject<{
|
|
@@ -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()
|
package/dist/mcp/gateway.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveRepoLocalBackendSpecs } from "./repo-local-backends.js";
|
|
2
2
|
import { renderToolHiveVmcpInventory } from "./toolhive-vmcp.js";
|
|
3
|
+
import { Data } from "effect";
|
|
3
4
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { homedir } from "node:os";
|
|
@@ -9,10 +10,9 @@ const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
|
|
|
9
10
|
const DEFAULT_LOCAL_GATEWAY_URL = "http://127.0.0.1:4483/mcp";
|
|
10
11
|
const LEGACY_OPENCODE_MCP_RE = /"mcp"\s*:\s*{(?!\s*"pipeline-gateway")/s;
|
|
11
12
|
const LEGACY_PIPELINE_MCP_RE = /path:\s*\.mcp\.json|uvx\s+mcpm|mcpm\s+run/;
|
|
12
|
-
var PipelineMcpGatewayError = class extends
|
|
13
|
+
var PipelineMcpGatewayError = class extends Data.TaggedError("PipelineMcpGatewayError") {
|
|
13
14
|
constructor(message) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = "PipelineMcpGatewayError";
|
|
15
|
+
super({ message });
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
18
|
function profileNeedsMcpGateway(actor) {
|
package/dist/moka-submit.d.ts
CHANGED
|
@@ -5,13 +5,13 @@ import { z } from "zod";
|
|
|
5
5
|
//#region src/moka-submit.d.ts
|
|
6
6
|
declare const mokaSubmitDirectHooksSchema: z.ZodRecord<z.ZodEnum<{
|
|
7
7
|
"workflow.start": "workflow.start";
|
|
8
|
+
"node.finish": "node.finish";
|
|
9
|
+
"node.start": "node.start";
|
|
8
10
|
"workflow.success": "workflow.success";
|
|
9
11
|
"workflow.failure": "workflow.failure";
|
|
10
12
|
"workflow.complete": "workflow.complete";
|
|
11
|
-
"node.start": "node.start";
|
|
12
13
|
"node.success": "node.success";
|
|
13
14
|
"node.error": "node.error";
|
|
14
|
-
"node.finish": "node.finish";
|
|
15
15
|
"gate.failure": "gate.failure";
|
|
16
16
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
17
17
|
failure: z.ZodDefault<z.ZodEnum<{
|
|
@@ -94,13 +94,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
94
94
|
}, z.core.$strict>>;
|
|
95
95
|
hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
96
96
|
"workflow.start": "workflow.start";
|
|
97
|
+
"node.finish": "node.finish";
|
|
98
|
+
"node.start": "node.start";
|
|
97
99
|
"workflow.success": "workflow.success";
|
|
98
100
|
"workflow.failure": "workflow.failure";
|
|
99
101
|
"workflow.complete": "workflow.complete";
|
|
100
|
-
"node.start": "node.start";
|
|
101
102
|
"node.success": "node.success";
|
|
102
103
|
"node.error": "node.error";
|
|
103
|
-
"node.finish": "node.finish";
|
|
104
104
|
"gate.failure": "gate.failure";
|
|
105
105
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
106
106
|
failure: z.ZodDefault<z.ZodEnum<{
|
|
@@ -206,13 +206,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
206
206
|
}, z.core.$strict>>;
|
|
207
207
|
hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
208
208
|
"workflow.start": "workflow.start";
|
|
209
|
+
"node.finish": "node.finish";
|
|
210
|
+
"node.start": "node.start";
|
|
209
211
|
"workflow.success": "workflow.success";
|
|
210
212
|
"workflow.failure": "workflow.failure";
|
|
211
213
|
"workflow.complete": "workflow.complete";
|
|
212
|
-
"node.start": "node.start";
|
|
213
214
|
"node.success": "node.success";
|
|
214
215
|
"node.error": "node.error";
|
|
215
|
-
"node.finish": "node.finish";
|
|
216
216
|
"gate.failure": "gate.failure";
|
|
217
217
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
218
218
|
failure: z.ZodDefault<z.ZodEnum<{
|