@oisincoveney/pipeline 2.7.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 (35) 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 +21 -4
  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 +1 -1
  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 +8 -3
  20. package/dist/runner-command-contract.js +6 -5
  21. package/dist/runner-event-sink.js +6 -5
  22. package/dist/runner.d.ts +6 -1
  23. package/dist/runner.js +3 -3
  24. package/dist/runtime/agent-node/agent-node.js +22 -4
  25. package/dist/runtime/local-scheduler.js +45 -0
  26. package/dist/runtime/opencode-server.js +6 -3
  27. package/dist/runtime/parallel-node/parallel-node.js +74 -75
  28. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
  29. package/dist/runtime/run-journal.js +21 -0
  30. package/dist/runtime/scheduler.js +122 -93
  31. package/dist/runtime/select-candidate/select-candidate.js +13 -1
  32. package/dist/runtime/services/worktree-service.js +18 -0
  33. package/dist/schedule/passes/candidates.js +17 -8
  34. package/docs/config-architecture.md +105 -0
  35. 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";
@@ -472,8 +481,8 @@ declare const configSchema: z.ZodObject<{
472
481
  schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
473
482
  description: z.ZodOptional<z.ZodString>;
474
483
  baseline: z.ZodEnum<{
475
- quick: "quick";
476
484
  execute: "execute";
485
+ quick: "quick";
477
486
  }>;
478
487
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
479
488
  node_catalog: z.ZodOptional<z.ZodString>;
@@ -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) {
@@ -160,8 +160,8 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
160
160
  }, z.core.$strict>>;
161
161
  serviceAccountName: z.ZodOptional<z.ZodString>;
162
162
  mode: z.ZodEnum<{
163
- full: "full";
164
163
  quick: "quick";
164
+ full: "full";
165
165
  }>;
166
166
  schedulePath: z.ZodOptional<z.ZodString>;
167
167
  scheduleYaml: z.ZodOptional<z.ZodString>;
@@ -2,7 +2,7 @@ import { installCommands } from "./install-commands.js";
2
2
  import { execa } from "execa";
3
3
  //#region src/pipeline-init.ts
4
4
  const DEFAULT_SKILL_INSTALL_SOURCE = "oisin-ee/skills";
5
- const DEFAULT_SKILL_INSTALL_ARGS = [
5
+ const SKILL_INSTALL_AGENT_ARGS = [
6
6
  "--agent",
7
7
  "opencode",
8
8
  "--agent",
@@ -11,17 +11,19 @@ const DEFAULT_SKILL_INSTALL_ARGS = [
11
11
  "claude-code",
12
12
  "--skill",
13
13
  "*",
14
- "--yes",
15
- "--copy"
14
+ "--yes"
16
15
  ];
17
- async function installDefaultSkills(cwd) {
16
+ function skillInstallArgs(scope) {
17
+ return scope === "personal" ? [...SKILL_INSTALL_AGENT_ARGS, "--global"] : [...SKILL_INSTALL_AGENT_ARGS, "--copy"];
18
+ }
19
+ async function installDefaultSkills(cwd, scope) {
18
20
  try {
19
21
  await execa("npx", [
20
22
  "--yes",
21
23
  "skills",
22
24
  "add",
23
25
  DEFAULT_SKILL_INSTALL_SOURCE,
24
- ...DEFAULT_SKILL_INSTALL_ARGS
26
+ ...skillInstallArgs(scope)
25
27
  ], {
26
28
  cwd,
27
29
  stdio: "inherit"
@@ -33,17 +35,21 @@ async function installDefaultSkills(cwd) {
33
35
  }
34
36
  async function initPipelineProject(options = {}) {
35
37
  const cwd = options.cwd ?? process.cwd();
36
- await (options.skillInstaller ?? installDefaultSkills)(cwd);
37
- return { files: (await installCommands({
38
- cwd,
39
- force: true,
40
- host: "all"
41
- })).items.map((item) => item.path) };
38
+ const scope = options.scope ?? "project";
39
+ await (options.skillInstaller ?? ((target) => installDefaultSkills(target, scope)))(cwd);
40
+ return {
41
+ files: (await installCommands({
42
+ cwd,
43
+ force: true,
44
+ host: "all"
45
+ })).items.map((item) => item.path),
46
+ scope
47
+ };
42
48
  }
43
49
  function formatPipelineInitResult(result) {
44
50
  return [
45
51
  "Initialized package-owned pipeline support:",
46
- "installed default skills",
52
+ result.scope === "personal" ? "installed default skills at user/global scope (inherited by every repo, no per-repo copy)" : "installed default skills (repo-local copy)",
47
53
  ...result.files.map((path) => `generated ${path}`),
48
54
  "no repo-local pipeline config files were created"
49
55
  ].join("\n");