@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.
Files changed (36) hide show
  1. package/defaults/pipeline.yaml +25 -0
  2. package/dist/argo-graph.js +7 -7
  3. package/dist/bench/eval-report.js +27 -0
  4. package/dist/cli/program.js +19 -3
  5. package/dist/commands/bench-command.js +18 -0
  6. package/dist/config/load.js +17 -0
  7. package/dist/config/schemas.d.ts +22 -5
  8. package/dist/config/schemas.js +20 -7
  9. package/dist/context/repo-map.js +203 -0
  10. package/dist/install-commands/opencode.js +10 -1
  11. package/dist/mcp/gateway.js +3 -3
  12. package/dist/moka-submit.d.ts +6 -6
  13. package/dist/pipeline-init.js +18 -12
  14. package/dist/pipeline-runtime.js +12 -1
  15. package/dist/planning/compile.d.ts +8 -3
  16. package/dist/planning/compile.js +7 -7
  17. package/dist/planning/generate.d.ts +6 -1
  18. package/dist/planning/generate.js +29 -7
  19. package/dist/runner-command-contract.d.ts +6 -1
  20. package/dist/runner-command-contract.js +6 -5
  21. package/dist/runner-event-schema.d.ts +6 -6
  22. package/dist/runner-event-sink.js +6 -5
  23. package/dist/runner.d.ts +6 -1
  24. package/dist/runner.js +3 -3
  25. package/dist/runtime/agent-node/agent-node.js +22 -4
  26. package/dist/runtime/local-scheduler.js +45 -0
  27. package/dist/runtime/opencode-server.js +6 -3
  28. package/dist/runtime/parallel-node/parallel-node.js +77 -58
  29. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
  30. package/dist/runtime/run-journal.js +21 -0
  31. package/dist/runtime/scheduler.js +122 -93
  32. package/dist/runtime/select-candidate/select-candidate.js +13 -1
  33. package/dist/runtime/services/worktree-service.js +18 -0
  34. package/dist/schedule/passes/candidates.js +17 -8
  35. package/docs/config-architecture.md +105 -0
  36. package/package.json +7 -2
@@ -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
@@ -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 Error {
22
- kind;
23
- nodeId;
22
+ var ArgoGraphCompilerError = class extends Data.TaggedError("ArgoGraphCompilerError") {
24
23
  constructor(kind, nodeId) {
25
- super(`Argo graph compiler: node kind '${kind}' on node '${nodeId}' cannot be lowered to an Argo DAG task`);
26
- this.name = "ArgoGraphCompilerError";
27
- this.kind = kind;
28
- this.nodeId = nodeId;
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 };
@@ -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
- async function runConfiguredPipeline(inputs) {
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({ cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd() });
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 };
@@ -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 } : {},
@@ -14,9 +14,14 @@ interface PipelineConfigIssue {
14
14
  message: string;
15
15
  path?: string;
16
16
  }
17
- declare class PipelineConfigError extends Error {
18
- code: PipelineConfigErrorCode;
19
- issues: PipelineConfigIssue[];
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>;
@@ -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 Error {
62
- code;
63
- issues;
62
+ var PipelineConfigError = class extends Data.TaggedError("PipelineConfigError") {
64
63
  constructor(code, message, issues = []) {
65
- super(message);
66
- this.name = "PipelineConfigError";
67
- this.code = code;
68
- this.issues = issues;
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.mcp_gateway ? JSON.parse(renderOpenCodeGatewayConfig(config)) : { $schema: "https://opencode.ai/config.json" },
271
+ ...shouldEmbedProjectGateway(config) ? JSON.parse(renderOpenCodeGatewayConfig(config)) : { $schema: "https://opencode.ai/config.json" },
263
272
  lsp: true,
264
273
  ...opencodePluginConfig(),
265
274
  ...opencodeProviderConfig()
@@ -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 Error {
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) {
@@ -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<{