@oisincoveney/pipeline 2.10.0 → 2.11.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/README.md CHANGED
@@ -98,6 +98,8 @@ Canonical commands:
98
98
  - `moka export <run-id> --sanitize`: print a portable evidence bundle.
99
99
  - `moka doctor`: check local prerequisites and config health.
100
100
  - `moka init`: install package-owned host resources for a repository.
101
+ - `moka refresh-harnesses`: force-refresh generated agent harnesses and commit
102
+ owned resource changes.
101
103
 
102
104
  ```shell
103
105
  moka run "Implement PIPE-123 user-facing behavior"
@@ -109,6 +111,7 @@ moka run --effort normal "Implement a standard fix"
109
111
  moka run --target remote --effort thorough "Submit a full hosted graph run"
110
112
  moka run --read-only "Inspect the repository without edits"
111
113
  moka run --target remote --command -- opencode run "fix this bug"
114
+ moka refresh-harnesses
112
115
  ```
113
116
 
114
117
  Flag defaults and choices:
@@ -119,6 +122,27 @@ Flag defaults and choices:
119
122
  - `--effort` selects `quick`, `normal`, or `thorough`; `normal` is the default.
120
123
  - `--read-only` switches mode to `read`; mode defaults to `write`.
121
124
 
125
+ Moka ticket selects and scopes Backlog work; moka run executes selected work.
126
+ Use Backlog CLI for task creation and editing instead of direct markdown edits.
127
+
128
+ ```shell
129
+ moka ticket graph check --root PIPE-84
130
+ moka ticket sequence --root PIPE-84 --plain
131
+ moka ticket next --root PIPE-84 --json
132
+ moka ticket next --claim --root PIPE-84
133
+ moka ticket create --dry-run "Plan a small Backlog task"
134
+ moka ticket create --apply --parent PIPE-84 "Plan and create child tasks"
135
+ moka ticket start --root PIPE-84
136
+ moka ticket start --dry-run --root PIPE-84 --effort quick --target local
137
+ ```
138
+
139
+ Read-only ticket commands are `moka ticket graph check`, `moka ticket sequence`,
140
+ `moka ticket next`, `moka ticket create --dry-run`, and
141
+ `moka ticket start --dry-run`. Commands that mutate or run work are
142
+ `moka ticket next --claim`, `moka ticket create --apply`, and
143
+ `moka ticket start` without `--dry-run`: they use Backlog CLI task creation and
144
+ editing or invoke `moka run` for the selected ticket.
145
+
122
146
  Local run artifacts live under `.pipeline/runs/<runId>/`:
123
147
 
124
148
  ```text
@@ -111,6 +111,20 @@ profiles:
111
111
  format: json_schema
112
112
  schema_path: .pipeline/schemas/research.schema.json
113
113
  repair: { enabled: true, max_attempts: 1 }
114
+ moka-ticket-scoper:
115
+ runner: opencode
116
+ description: Scope a request into a validated Backlog ticket plan.
117
+ instructions: { inline: "Use the scope skill contract to produce implementation-complete Backlog tickets. Return only valid JSON matching `.pipeline/schemas/ticket-plan.schema.json`. Every acceptance criterion must include concrete evidence text. Do not emit partial tickets; include dependencies, likely files, references, priorities, and plan text for each proposed ticket." }
118
+ timeout_ms: 300000
119
+ skills: [scope]
120
+ mcp_servers: [pipeline-gateway]
121
+ tools: [read, list, grep, glob, bash]
122
+ filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
123
+ network: { mode: inherit }
124
+ output:
125
+ format: json_schema
126
+ schema_path: .pipeline/schemas/ticket-plan.schema.json
127
+ repair: { enabled: true, max_attempts: 1 }
114
128
  moka-inspector:
115
129
  runner: opencode
116
130
  model: openai/gpt-5.5-low
@@ -0,0 +1,13 @@
1
+ //#region src/backlog.ts
2
+ /**
3
+ * `backlog task create` (with `--plain`) prints `Task <PREFIX>-<id> - <title>`
4
+ * on the second non-blank line. We accept custom all-caps Backlog prefixes and
5
+ * subtask ids like `PIPE-3.1`.
6
+ */
7
+ const TASK_ID_RE = /^Task\s+([A-Z]+-[\w.]+)\b/m;
8
+ function parseBacklogTaskId(stdout) {
9
+ const m = TASK_ID_RE.exec(stdout);
10
+ return m ? m[1] : null;
11
+ }
12
+ //#endregion
13
+ export { parseBacklogTaskId };
@@ -1,5 +1,7 @@
1
- import { runPipelineFromConfig } from "../pipeline-runtime.js";
2
1
  import { RunEffort, RunMode, RunTarget } from "../run-control/contracts.js";
2
+ import { RunCommand } from "./run-command.js";
3
+ import { TicketCommandOptions } from "../commands/ticket-command.js";
4
+ import { runPipelineFromConfig } from "../pipeline-runtime.js";
3
5
  import { Effect } from "effect";
4
6
  import { Command } from "commander";
5
7
 
@@ -27,7 +29,11 @@ interface RunControlOptions {
27
29
  */
28
30
  declare function execute(description: string, options?: ExecuteOptions): Promise<void>;
29
31
  declare function quick(description: string, options?: Omit<ExecuteOptions, "entrypoint">): Promise<void>;
30
- declare function createCliProgram(): Command;
32
+ interface CliProgramOptions {
33
+ readonly runCommand?: RunCommand;
34
+ readonly ticketCommand?: TicketCommandOptions;
35
+ }
36
+ declare function createCliProgram(options?: CliProgramOptions): Command;
31
37
  declare function runCli(argv: string[]): Promise<void>;
32
38
  //#endregion
33
39
  export { createCliProgram, execute, quick, runCli };
@@ -11,9 +11,11 @@ import { formatCodexAuthSyncResult, syncLocalCodexAuth } from "../codex-auth-syn
11
11
  import { registerBenchCommand } from "../commands/bench-command.js";
12
12
  import { registerConfiguredEntrypointCommands } from "../commands/pipeline-command.js";
13
13
  import { registerRunnerCommandCommand } from "../commands/runner-command-command.js";
14
+ import { MOKA_RUN_EFFORTS, MOKA_RUN_TARGETS, resolveMokaRun } from "./run-resolver.js";
15
+ import { registerTicketCommand } from "../commands/ticket-command.js";
14
16
  import { formatConfigLintWarning, lintPipelineConfig } from "../config/lint.js";
15
17
  import { formatInstallCommandsResult, installCommands, parseCommandHost } from "../install-commands.js";
16
- import { formatPipelineInitResult, initPipelineProject } from "../pipeline-init.js";
18
+ import { formatPipelineInitResult, formatRefreshAgentHarnessesResult, initPipelineProject, refreshAgentHarnesses } from "../pipeline-init.js";
17
19
  import { createRun, runControlStatusPaths, updateRunController } from "../run-control/store.js";
18
20
  import { registerRunControlCommands } from "../run-control/commands.js";
19
21
  import { startDetachedRunController } from "../run-control/detach.js";
@@ -21,7 +23,7 @@ import { createRunStoreRuntimeReporter } from "../run-control/runtime-reporter.j
21
23
  import { createRunControlSupervisor } from "../run-control/supervisor.js";
22
24
  import { runDoctor } from "./doctor.js";
23
25
  import { createTerminalRuntimeReporter, formatDoctorResult, formatRuntimeFailure, formatRuntimeResult } from "./format.js";
24
- import { MOKA_RUN_EFFORTS, MOKA_RUN_TARGETS, resolveMokaRun } from "./run-resolver.js";
26
+ import { dispatchMokaRunCommand } from "./run-command.js";
25
27
  import { addMokaSubmitOptions, runMokaSubmitFromCli } from "./submit-options.js";
26
28
  import { Effect } from "effect";
27
29
  import { readFileSync } from "node:fs";
@@ -231,30 +233,31 @@ function scheduledEntrypointId(config, workflowId, entrypointId) {
231
233
  const entrypoint = config.entrypoints[id];
232
234
  return entrypoint && "schedule" in entrypoint ? id : null;
233
235
  }
234
- function createCliProgram() {
236
+ function createCliProgram(options = {}) {
235
237
  const configuredPipeline = loadConfiguredEntrypoints(process.env.PIPELINE_TARGET_PATH ?? process.cwd());
236
238
  const program = new Command();
237
239
  program.name("moka").description("Submit work to Momokaya").version(readPackageVersion()).exitOverride();
240
+ const dispatchResolvedRunCommand = async (call) => {
241
+ await dispatchMokaRunCommand(call, {
242
+ runCommand: options.runCommand,
243
+ runDetached: ({ execution, runControl, task: resolvedTask }) => runDetachedResolvedTask(resolvedTask, execution, runControl),
244
+ runLocal: ({ execution, runControl, task: resolvedTask }) => runLocalResolvedTask(resolvedTask, execution, runControl),
245
+ runRemoteSubmit: async ({ descriptionParts: parts, execution }) => {
246
+ printMokaSubmitResult(await runMokaSubmitFromCli(parts, remoteSubmitFlags(execution)));
247
+ }
248
+ });
249
+ };
238
250
  const runAction = async (descriptionParts, flags) => {
239
251
  const task = descriptionParts.join(" ");
240
- const resolution = resolveMokaRun({
252
+ await dispatchResolvedRunCommand({
253
+ descriptionParts,
241
254
  flags,
255
+ resolution: resolveMokaRun({
256
+ flags,
257
+ task
258
+ }),
242
259
  task
243
260
  });
244
- if (resolution.execution.kind === "remote-submit") {
245
- printMokaSubmitResult(await runMokaSubmitFromCli(descriptionParts, remoteSubmitFlags(resolution.execution)));
246
- return;
247
- }
248
- const runControl = {
249
- effort: resolution.effort,
250
- mode: resolution.mode === "read" ? "read-only" : "write",
251
- target: resolution.target
252
- };
253
- if (flags.detach) {
254
- await runDetachedResolvedTask(task, resolution.execution, runControl);
255
- return;
256
- }
257
- await runLocalResolvedTask(task, resolution.execution, runControl);
258
261
  };
259
262
  program.command("run").description("Primary command: run a workflow from package-owned @oisincoveney/pipeline config").argument("<description...>", "task description").option("--command", "treat input after -- as explicit argv for remote submission").option("--entrypoint <entrypoint>", "entrypoint alias from package config").option("--detach", "start a supervised controller process in the background").addOption(new Option$1("--effort <effort>", "run effort").choices([...MOKA_RUN_EFFORTS]).default("normal")).option("--read-only", "run the read-only inspect workflow").option("--schedule <schedule>", "approved schedule YAML to execute").addOption(new Option$1("--target <target>", "execution target").choices([...MOKA_RUN_TARGETS]).default("local")).option("--workflow <workflow>", "workflow id from package config").action(runAction);
260
263
  program.command("run-controller", { hidden: true }).description("Internal detached run controller").argument("<description...>", "task description").requiredOption("--run-id <run-id>", "existing run id to supervise").option("--entrypoint <entrypoint>", "entrypoint alias from package config").option("--schedule <schedule>", "approved schedule YAML to execute").option("--workflow <workflow>", "workflow id from package config").action(async (descriptionParts, flags) => {
@@ -334,6 +337,14 @@ function createCliProgram() {
334
337
  });
335
338
  console.log(formatPipelineInitResult(result));
336
339
  });
340
+ program.command("refresh-harnesses").description("Force-refresh generated agent harnesses and commit owned resource changes").addOption(new Option$1("--skill-scope <scope>", "where to install default skills: project (repo-local copy) or personal (one inherited user/global install)").choices(["project", "personal"]).default("project")).option("--message <message>", "git commit message for refreshed harness changes", "chore: update agent harnesses").action(async (flags) => {
341
+ const result = await refreshAgentHarnesses({
342
+ commitMessage: flags.message,
343
+ cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
344
+ scope: flags.skillScope
345
+ });
346
+ console.log(formatRefreshAgentHarnessesResult(result));
347
+ });
337
348
  program.command("install-commands").description("Install generated slash-command adapters into this repository").addOption(new Option$1("--host <host>", "host command set to install").choices([
338
349
  "all",
339
350
  "opencode",
@@ -364,6 +375,10 @@ function createCliProgram() {
364
375
  });
365
376
  registerRunnerCommandCommand(program);
366
377
  registerBenchCommand(program);
378
+ registerTicketCommand(program, {
379
+ ...options.ticketCommand,
380
+ runCommand: options.ticketCommand?.runCommand ?? dispatchResolvedRunCommand
381
+ });
367
382
  const configuredEntrypointCommands = registerConfiguredEntrypointCommands(program, compatibilityPresetDescriptions(configuredPipeline), async (entrypoint, task, _opts) => {
368
383
  await execute(task, { entrypoint });
369
384
  });
@@ -0,0 +1,12 @@
1
+ import { RunResolution, RunResolverFlags } from "./run-resolver.js";
2
+
3
+ //#region src/cli/run-command.d.ts
4
+ interface RunCommandCall {
5
+ readonly descriptionParts: string[];
6
+ readonly flags: RunResolverFlags;
7
+ readonly resolution: RunResolution;
8
+ readonly task: string;
9
+ }
10
+ type RunCommand = (call: RunCommandCall) => Promise<void> | void;
11
+ //#endregion
12
+ export { RunCommand };
@@ -0,0 +1,42 @@
1
+ //#region src/cli/run-command.ts
2
+ async function dispatchMokaRunCommand(call, dependencies) {
3
+ if (dependencies.runCommand) {
4
+ await dependencies.runCommand(call);
5
+ return;
6
+ }
7
+ await dispatchResolvedMokaRunCommand(call, dependencies);
8
+ }
9
+ async function dispatchResolvedMokaRunCommand(call, dependencies) {
10
+ const { resolution } = call;
11
+ const { execution } = resolution;
12
+ if (execution.kind === "remote-submit") {
13
+ await dependencies.runRemoteSubmit({
14
+ descriptionParts: call.descriptionParts,
15
+ execution
16
+ });
17
+ return;
18
+ }
19
+ await dispatchLocalMokaRunCommand(call, execution, dependencies);
20
+ }
21
+ async function dispatchLocalMokaRunCommand(call, execution, dependencies) {
22
+ const localDispatchInput = localRunDispatchInput(call, execution);
23
+ if (call.flags.detach) {
24
+ await dependencies.runDetached(localDispatchInput);
25
+ return;
26
+ }
27
+ await dependencies.runLocal(localDispatchInput);
28
+ }
29
+ function localRunDispatchInput(call, execution) {
30
+ const { resolution, task } = call;
31
+ return {
32
+ execution,
33
+ runControl: {
34
+ effort: resolution.effort,
35
+ mode: resolution.mode === "read" ? "read-only" : "write",
36
+ target: resolution.target
37
+ },
38
+ task
39
+ };
40
+ }
41
+ //#endregion
42
+ export { dispatchMokaRunCommand };
@@ -0,0 +1,36 @@
1
+ //#region src/cli/run-resolver.d.ts
2
+ declare const MOKA_RUN_EFFORTS: readonly ["normal", "quick", "thorough"];
3
+ declare const MOKA_RUN_TARGETS: readonly ["local", "remote"];
4
+ type MokaRunEffort = (typeof MOKA_RUN_EFFORTS)[number];
5
+ type MokaRunTarget = (typeof MOKA_RUN_TARGETS)[number];
6
+ type MokaRunMode = "read" | "write";
7
+ interface RunResolverFlags {
8
+ command?: boolean;
9
+ detach?: boolean;
10
+ effort?: MokaRunEffort;
11
+ entrypoint?: string;
12
+ readOnly?: boolean;
13
+ schedule?: string;
14
+ target?: MokaRunTarget;
15
+ workflow?: string;
16
+ }
17
+ interface LocalRuntimeExecution {
18
+ entrypoint?: string;
19
+ kind: "local-runtime";
20
+ schedule?: string;
21
+ workflow?: string;
22
+ }
23
+ interface RemoteSubmitExecution {
24
+ command?: boolean;
25
+ kind: "remote-submit";
26
+ mode: "full" | "quick";
27
+ schedule?: string;
28
+ }
29
+ interface RunResolution {
30
+ effort: MokaRunEffort;
31
+ execution: LocalRuntimeExecution | RemoteSubmitExecution;
32
+ mode: MokaRunMode;
33
+ target: MokaRunTarget;
34
+ }
35
+ //#endregion
36
+ export { RunResolution, RunResolverFlags };
@@ -6,11 +6,13 @@ const BUILTIN_PIPE_COMMANDS = new Set([
6
6
  "explain-plan",
7
7
  "doctor",
8
8
  "init",
9
+ "refresh-harnesses",
9
10
  "install-commands",
10
11
  "mcp",
11
12
  "submit",
12
13
  "argo",
13
- "runner-command"
14
+ "runner-command",
15
+ "ticket"
14
16
  ]);
15
17
  var EntrypointCommandService = class extends Context.Tag("EntrypointCommandService")() {};
16
18
  const createEntrypointCommandServiceLive = (runEntrypoint) => Layer.succeed(EntrypointCommandService, { runEntrypoint: (entrypoint, task, opts) => Effect.tryPromise({
@@ -0,0 +1,15 @@
1
+ import { RunCommand } from "../cli/run-command.js";
2
+ import { AgentResult, RunnerExecutionOptions, RunnerLaunchPlan } from "../runner.js";
3
+ import { BacklogService } from "../runtime/services/backlog-service.js";
4
+ import { Layer } from "effect";
5
+ import { Command } from "commander";
6
+
7
+ //#region src/commands/ticket-command.d.ts
8
+ type TicketPlanExecutor = (plan: RunnerLaunchPlan, options: RunnerExecutionOptions) => Promise<AgentResult>;
9
+ interface TicketCommandOptions {
10
+ readonly backlogLayer?: Layer.Layer<BacklogService>;
11
+ readonly runCommand?: RunCommand;
12
+ readonly ticketPlanExecutor?: TicketPlanExecutor;
13
+ }
14
+ //#endregion
15
+ export { TicketCommandOptions };
@@ -0,0 +1,308 @@
1
+ import { parseTicketPlanEffect } from "../tickets/ticket-plan.js";
2
+ import { loadPipelineConfig } from "../config/load.js";
3
+ import "../config.js";
4
+ import { RepoIoServiceLive } from "../runtime/services/repo-io-service.js";
5
+ import { createRunnerLaunchPlan, runLaunchPlan } from "../runner.js";
6
+ import { normalizeRunnerOutput } from "../runner-output.js";
7
+ import { MOKA_RUN_EFFORTS, MOKA_RUN_TARGETS, resolveMokaRun } from "../cli/run-resolver.js";
8
+ import { BacklogService, BacklogServiceLive } from "../runtime/services/backlog-service.js";
9
+ import { applyTicketPlanEffect } from "../tickets/apply-ticket-plan.js";
10
+ import { loadBacklogTaskStoreEffect } from "../tickets/backlog-task-store.js";
11
+ import { buildTicketGraphEffect, scopedTicketIds, sequenceTicketBatchesEffect } from "../tickets/ticket-graph.js";
12
+ import { renderTicketPlanDryRun } from "../tickets/ticket-plan-render.js";
13
+ import { selectNextTicket, selectReadyTickets } from "../tickets/ticket-selection.js";
14
+ import { Data, Effect } from "effect";
15
+ import { Option as Option$1 } from "commander";
16
+ //#region src/commands/ticket-command.ts
17
+ var TicketCommandError = class extends Data.TaggedError("TicketCommandError") {};
18
+ const TICKET_SCOPER_PROFILE = "moka-ticket-scoper";
19
+ const TICKET_SELECTION_STRATEGIES = new Set([
20
+ "priority",
21
+ "bfs",
22
+ "dfs"
23
+ ]);
24
+ const TICKET_CREATE_FLAG_RULES = [
25
+ {
26
+ invalid: (flags) => Boolean(flags.dryRun && flags.apply),
27
+ message: "moka ticket create accepts only one of --dry-run or --apply"
28
+ },
29
+ {
30
+ invalid: (flags) => !(flags.dryRun || flags.apply),
31
+ message: "moka ticket create requires --dry-run or --apply"
32
+ },
33
+ {
34
+ invalid: (flags) => Boolean(flags.parent && !flags.apply),
35
+ message: "moka ticket create --parent is only valid with --apply"
36
+ }
37
+ ];
38
+ function registerTicketCommand(program, options = {}) {
39
+ const ticketCommand = program.command("ticket").description("Scope, inspect, and select Backlog tickets for moka runs");
40
+ ticketCommand.command("graph").description("Inspect the Backlog ticket dependency graph").command("check").description("Validate Backlog ticket dependency references and cycles").option("--root <ticket-id>", "limit validation summary to one ticket tree").action((flags) => runTicketProgram(checkTicketGraphEffect(currentWorktreePath(), flags)));
41
+ ticketCommand.command("sequence").description("Print dependency execution batches for Backlog tickets").option("--root <ticket-id>", "sequence one ticket tree").option("--plain", "print plain text output").action((flags) => runTicketProgram(printTicketSequenceEffect(currentWorktreePath(), flags)));
42
+ ticketCommand.command("next").description("Select the next ready Backlog ticket deterministically").option("--root <ticket-id>", "select from one ticket tree").option("--claim", "mark the selected ticket In Progress through Backlog").option("--include-parents", "allow parent tickets in selection results").option("--json", "print machine-readable selection output").option("--strategy <strategy>", "selection strategy: priority, bfs, or dfs").action((flags) => runTicketProgramWithBacklog(printNextTicketEffect(currentWorktreePath(), flags), options.backlogLayer ?? BacklogServiceLive));
43
+ ticketCommand.command("start").description("Claim the next ready Backlog ticket and run moka").option("--root <ticket-id>", "select from one ticket tree").option("--include-parents", "allow parent tickets in selection results").option("--strategy <strategy>", "selection strategy: priority, bfs, or dfs").option("--dry-run", "print the selected moka run command without claiming").addOption(new Option$1("--effort <effort>", "run effort").choices([...MOKA_RUN_EFFORTS]).default("normal")).addOption(new Option$1("--target <target>", "execution target").choices([...MOKA_RUN_TARGETS]).default("local")).option("--read-only", "run the read-only inspect workflow").action((flags) => runTicketProgramWithBacklog(startTicketEffect(currentWorktreePath(), flags, options.runCommand), options.backlogLayer ?? BacklogServiceLive));
44
+ ticketCommand.command("create").description("Create a validated Backlog ticket plan").argument("<request...>", "ticket planning request").option("--dry-run", "render Backlog commands without writing tasks").option("--apply", "apply the validated ticket plan through Backlog").option("--parent <task-id>", "existing parent task for applied children").action((requestParts, flags) => Effect.runPromise(Effect.provide(printTicketCreateEffect(currentWorktreePath(), requestParts.join(" "), flags, options.ticketPlanExecutor ?? runLaunchPlan), options.backlogLayer ?? BacklogServiceLive)));
45
+ }
46
+ function startTicketEffect(worktreePath, flags, runCommand) {
47
+ return Effect.gen(function* () {
48
+ const { loaded, selectionOptions } = yield* loadTicketSelectionEffect(worktreePath, flags);
49
+ const selected = yield* readyTicketEffect(selectNextTicket(loaded.graph, selectionOptions));
50
+ const task = ticketRunTask(selected);
51
+ const descriptionParts = [task];
52
+ const runFlags = yield* ticketStartRunFlagsEffect(flags);
53
+ const resolution = yield* Effect.try({
54
+ catch: (error) => new TicketCommandError({ message: `Could not resolve moka run for ticket '${selected.id}': ${errorMessage(error)}` }),
55
+ try: () => resolveMokaRun({
56
+ flags: runFlags,
57
+ task
58
+ })
59
+ });
60
+ yield* writeLineEffect(`Selected ticket: ${formatNextTicket(selected)}`);
61
+ if (flags.dryRun) {
62
+ yield* writeLineEffect(formatTicketStartDryRun(runFlags, task));
63
+ return;
64
+ }
65
+ if (!runCommand) return yield* Effect.fail(new TicketCommandError({ message: "Could not start moka run: no run dispatcher configured." }));
66
+ yield* claimTicketEffect(worktreePath, selected);
67
+ yield* Effect.tryPromise({
68
+ catch: (error) => new TicketCommandError({ message: `Could not start moka run for ticket '${selected.id}': ${errorMessage(error)}` }),
69
+ try: async () => {
70
+ await runCommand({
71
+ descriptionParts,
72
+ flags: runFlags,
73
+ resolution,
74
+ task
75
+ });
76
+ }
77
+ });
78
+ });
79
+ }
80
+ function ticketRunTask(ticket) {
81
+ const title = formatNextTicket(ticket);
82
+ const description = ticket.description?.trim();
83
+ return description ? `${title}\n\n${description}` : title;
84
+ }
85
+ function ticketStartRunFlagsEffect(flags) {
86
+ return Effect.gen(function* () {
87
+ yield* validateTicketStartFlagsEffect(flags);
88
+ return ticketStartRunFlags(flags);
89
+ });
90
+ }
91
+ function validateTicketStartFlagsEffect(flags) {
92
+ return flags.readOnly && flags.target === "remote" ? Effect.fail(new TicketCommandError({ message: "moka ticket start --read-only cannot be combined with --target remote." })) : Effect.void;
93
+ }
94
+ function ticketStartRunFlags(flags) {
95
+ return {
96
+ effort: flags.effort ?? "normal",
97
+ readOnly: flags.readOnly,
98
+ target: flags.target ?? "local"
99
+ };
100
+ }
101
+ function formatTicketStartDryRun(flags, task) {
102
+ return [
103
+ "moka run",
104
+ `--effort ${flags.effort ?? "normal"}`,
105
+ `--target ${flags.target ?? "local"}`,
106
+ flags.readOnly ? "--read-only" : "",
107
+ shellQuote(task)
108
+ ].filter(Boolean).join(" ");
109
+ }
110
+ function shellQuote(value) {
111
+ return `'${value.replaceAll("'", "'\\''")}'`;
112
+ }
113
+ function printTicketCreateEffect(worktreePath, request, flags, executor) {
114
+ return Effect.gen(function* () {
115
+ yield* validateTicketCreateFlagsEffect(flags);
116
+ const ticketPlan = yield* parseTicketPlanEffect(yield* runTicketScoperEffect(yield* ticketScoperLaunchPlanEffect(worktreePath, request), executor));
117
+ if (flags.dryRun) {
118
+ yield* writeLineEffect(renderTicketPlanDryRun(ticketPlan));
119
+ return;
120
+ }
121
+ yield* writeLineEffect(formatAppliedTicketPlan(yield* applyTicketPlanEffect(ticketPlan, worktreePath, { parentId: flags.parent })));
122
+ });
123
+ }
124
+ function validateTicketCreateFlagsEffect(flags) {
125
+ const message = ticketCreateFlagErrorMessage(flags);
126
+ return message ? Effect.fail(new TicketCommandError({ message })) : Effect.void;
127
+ }
128
+ function ticketCreateFlagErrorMessage(flags) {
129
+ return TICKET_CREATE_FLAG_RULES.find((rule) => rule.invalid(flags))?.message;
130
+ }
131
+ function ticketScoperLaunchPlanEffect(worktreePath, request) {
132
+ return Effect.gen(function* () {
133
+ const config = yield* Effect.try({
134
+ catch: (error) => new TicketCommandError({ message: `Could not load pipeline config: ${errorMessage(error)}` }),
135
+ try: () => loadPipelineConfig(worktreePath, { allowMissingLintFileReferences: true })
136
+ });
137
+ return yield* Effect.try({
138
+ catch: (error) => new TicketCommandError({ message: `Could not create ticket scoper launch plan: ${errorMessage(error)}` }),
139
+ try: () => createRunnerLaunchPlan(config, {
140
+ nodeId: "ticket-plan",
141
+ profileId: TICKET_SCOPER_PROFILE,
142
+ prompt: ticketPlanPrompt(request),
143
+ worktreePath
144
+ })
145
+ });
146
+ });
147
+ }
148
+ function runTicketScoperEffect(launchPlan, executor) {
149
+ return Effect.gen(function* () {
150
+ const result = yield* Effect.tryPromise({
151
+ catch: (error) => new TicketCommandError({ message: `Ticket scoper failed: ${errorMessage(error)}` }),
152
+ try: () => executor(launchPlan, {})
153
+ });
154
+ if (result.exitCode !== 0) return yield* Effect.fail(new TicketCommandError({ message: ticketScoperFailureMessage(result) }));
155
+ return normalizeRunnerOutput(launchPlan, result.stdout).output.trim();
156
+ });
157
+ }
158
+ function checkTicketGraphEffect(worktreePath, flags) {
159
+ return Effect.gen(function* () {
160
+ yield* writeLineEffect(`OK: ticket graph valid (${(yield* loadTicketGraphEffect(worktreePath, flags.root)).scopedIds.length} tickets)`);
161
+ });
162
+ }
163
+ function printTicketSequenceEffect(worktreePath, flags) {
164
+ return Effect.gen(function* () {
165
+ const loaded = yield* loadTicketGraphEffect(worktreePath, flags.root);
166
+ yield* writeLineEffect(formatSequence(yield* sequenceTicketBatchesEffect(loaded.graph, loaded.scopedIds)));
167
+ });
168
+ }
169
+ function printNextTicketEffect(worktreePath, flags) {
170
+ return Effect.gen(function* () {
171
+ const { loaded, selectionOptions } = yield* loadTicketSelectionEffect(worktreePath, flags);
172
+ const selected = selectNextTicket(loaded.graph, selectionOptions);
173
+ if (flags.claim) {
174
+ const ticket = yield* readyTicketEffect(selected);
175
+ yield* claimTicketEffect(worktreePath, ticket);
176
+ yield* writeLineEffect(`Claimed ${formatNextTicket(ticket)}`);
177
+ return;
178
+ }
179
+ const ready = selectReadyTickets(loaded.graph, selectionOptions);
180
+ yield* writeLineEffect(flags.json ? JSON.stringify({
181
+ ready: ready.map(ticketJson),
182
+ selected: ticketJson(selected)
183
+ }) : formatNextTicket(selected));
184
+ });
185
+ }
186
+ function claimTicketEffect(worktreePath, ticket) {
187
+ return Effect.gen(function* () {
188
+ yield* (yield* BacklogService).run([
189
+ "task",
190
+ "edit",
191
+ ticket.id,
192
+ "--status",
193
+ "In Progress",
194
+ "--plain"
195
+ ], worktreePath).pipe(Effect.mapError((error) => new TicketCommandError({ message: `Could not claim ticket '${ticket.id}': ${errorMessage(error)}` })));
196
+ });
197
+ }
198
+ function loadTicketGraphEffect(worktreePath, rootId) {
199
+ return Effect.gen(function* () {
200
+ const graph = yield* buildTicketGraphEffect((yield* loadBacklogTaskStoreEffect(worktreePath)).tasks);
201
+ const scopedIds = scopedTicketIds(graph, rootId);
202
+ if (rootId && scopedIds.length === 0) return yield* Effect.fail(new TicketCommandError({ message: `Unknown Backlog ticket '${rootId}'` }));
203
+ return {
204
+ graph,
205
+ scopedIds
206
+ };
207
+ });
208
+ }
209
+ function loadTicketSelectionEffect(worktreePath, flags) {
210
+ return Effect.gen(function* () {
211
+ const strategy = yield* parseSelectionStrategyEffect(flags.strategy);
212
+ return {
213
+ loaded: yield* loadTicketGraphEffect(worktreePath, flags.root),
214
+ selectionOptions: {
215
+ includeParents: flags.includeParents,
216
+ rootId: flags.root,
217
+ strategy
218
+ }
219
+ };
220
+ });
221
+ }
222
+ function parseSelectionStrategyEffect(strategy) {
223
+ return strategy === void 0 || isTicketSelectionStrategy(strategy) ? Effect.succeed(strategy) : Effect.fail(new TicketCommandError({ message: `Unknown ticket selection strategy '${strategy}'; expected priority, bfs, or dfs` }));
224
+ }
225
+ function isTicketSelectionStrategy(strategy) {
226
+ return TICKET_SELECTION_STRATEGIES.has(strategy);
227
+ }
228
+ function formatSequence(batches) {
229
+ return batches.map((batch, index) => [`Sequence ${index + 1}:`, ...batch.map((id) => ` ${id}`)].join("\n")).join("\n\n");
230
+ }
231
+ function formatNextTicket(ticket) {
232
+ return ticket ? `${ticket.id} - ${ticket.title}` : "No ready tickets.";
233
+ }
234
+ function formatAppliedTicketPlan(applied) {
235
+ return ["Created tickets:", ...Object.entries(applied.taskIdsByKey).map(([key, taskId]) => ` ${key}: ${taskId}`)].join("\n");
236
+ }
237
+ function ticketJson(ticket) {
238
+ if (!ticket) return null;
239
+ return {
240
+ acceptanceCriteria: ticket.acceptanceCriteria,
241
+ dependencies: ticket.dependencies,
242
+ description: ticket.description,
243
+ id: ticket.id,
244
+ modifiedFiles: ticket.modifiedFiles,
245
+ ordinal: ticket.ordinal,
246
+ parentTaskId: ticket.parentTaskId,
247
+ priority: ticket.priority,
248
+ references: ticket.references,
249
+ status: ticket.status,
250
+ title: ticket.title
251
+ };
252
+ }
253
+ function writeLineEffect(line) {
254
+ return Effect.sync(() => console.log(line));
255
+ }
256
+ function readyTicketEffect(ticket) {
257
+ return ticket ? Effect.succeed(ticket) : Effect.fail(new TicketCommandError({ message: "No ready tickets." }));
258
+ }
259
+ function currentWorktreePath() {
260
+ return process.env.PIPELINE_TARGET_PATH ?? process.cwd();
261
+ }
262
+ function runTicketProgram(program) {
263
+ return Effect.runPromise(Effect.provide(program, RepoIoServiceLive));
264
+ }
265
+ function runTicketProgramWithBacklog(program, backlogLayer) {
266
+ return Effect.runPromise(Effect.provide(Effect.provide(program, RepoIoServiceLive), backlogLayer));
267
+ }
268
+ function ticketPlanPrompt(request) {
269
+ return [
270
+ "Use the scope skill contract to produce a complete Backlog ticket plan.",
271
+ "Return only JSON matching this exact snake_case shape. Do not emit Markdown.",
272
+ "{\"epic\":{\"key\":\"epic\",\"title\":\"...\",\"description\":\"...\",\"priority\":\"high|medium|low\",\"acceptance_criteria\":[{\"text\":\"...\",\"evidence\":\"...\"}],\"likely_files\":[\"path\"],\"references\":[\"path-or-url\"],\"plan\":\"...\"},\"tickets\":[{\"key\":\"local-key\",\"title\":\"...\",\"description\":\"...\",\"priority\":\"high|medium|low\",\"depends_on\":[\"other-local-key\"],\"acceptance_criteria\":[{\"text\":\"...\",\"evidence\":\"...\"}],\"likely_files\":[\"path\"],\"references\":[\"path-or-url\"],\"plan\":\"...\"}]}",
273
+ "Omit epic entirely when no epic should be created.",
274
+ "Use depends_on only for local ticket keys from the same tickets array.",
275
+ "Every acceptance_criteria entry must include concrete evidence text.",
276
+ "Do not use epics, type, dependencies, labels, acceptanceCriteria, qualityGate, or camelCase keys.",
277
+ "Do not return partial tickets; if the request is unclear, encode the missing decision in the plan text instead of omitting required fields.",
278
+ "Task request:",
279
+ request
280
+ ].join("\n");
281
+ }
282
+ function ticketScoperFailureMessage(result) {
283
+ return runnerFailureMessage(`ticket scoper '${TICKET_SCOPER_PROFILE}'`, "timed out waiting for ticket scoper", result);
284
+ }
285
+ function runnerFailureMessage(label, timeoutMessage, result) {
286
+ const details = runnerFailureDetails(timeoutMessage, result);
287
+ const message = `${label} failed with exit ${result.exitCode}`;
288
+ return details.length === 0 ? message : `${message}\n${details.join("\n")}`;
289
+ }
290
+ function runnerFailureDetails(timeoutMessage, result) {
291
+ const details = [];
292
+ appendDetail(details, result.timedOut ? timeoutMessage : void 0);
293
+ appendDetail(details, labelledOutput("stderr", result.stderr));
294
+ appendDetail(details, labelledOutput("stdout", result.stdout));
295
+ return details;
296
+ }
297
+ function labelledOutput(label, value) {
298
+ const trimmed = value?.trim();
299
+ return trimmed ? `${label}:\n${trimmed}` : void 0;
300
+ }
301
+ function appendDetail(details, detail) {
302
+ if (detail) details.push(detail);
303
+ }
304
+ function errorMessage(error) {
305
+ return error instanceof Error ? error.message : String(error);
306
+ }
307
+ //#endregion
308
+ export { registerTicketCommand };
@@ -481,8 +481,8 @@ declare const configSchema: z.ZodObject<{
481
481
  schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
482
482
  description: z.ZodOptional<z.ZodString>;
483
483
  baseline: z.ZodEnum<{
484
- execute: "execute";
485
484
  quick: "quick";
485
+ execute: "execute";
486
486
  }>;
487
487
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
488
488
  node_catalog: z.ZodOptional<z.ZodString>;
@@ -355,8 +355,9 @@ function projectAgentsMdDefinition(cwd, host) {
355
355
  "",
356
356
  `Use Qdrant collection \`${repoName}\` for this repository.`,
357
357
  "",
358
- `- Call \`qdrant-find\` before research with \`collection_name: ${repoName}\` unless the user explicitly disables memory.`,
359
- `- Call \`qdrant-store\` during LEARN with \`collection_name: ${repoName}\` for durable lessons worth reusing.`,
358
+ "- Use the Qdrant interface exposed by the active host; do not assume `qdrant-find` or `qdrant-store` are shell commands.",
359
+ `- Before research, call MCP tool \`qdrant_qdrant_find\` with \`collection_name: ${repoName}\` when MCP tools are available; otherwise use the host's \`qdrant-find\` command/alias if one exists.`,
360
+ `- During LEARN, call MCP tool \`qdrant_qdrant_store\` with \`collection_name: ${repoName}\` for durable lessons worth reusing; otherwise use the host's \`qdrant-store\` command/alias if one exists.`,
360
361
  "- Include metadata with at least `repo`, `phase`, `workflow` or `entrypoint`, `task`, and `outcome` when storing lessons.",
361
362
  "",
362
363
  AGENTS_MD_END,