@oisincoveney/pipeline 2.9.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 +24 -0
- package/defaults/opencode-ecosystem.yaml +3 -3
- package/defaults/profiles.yaml +17 -0
- package/dist/backlog.js +13 -0
- package/dist/cli/program.d.ts +8 -2
- package/dist/cli/program.js +33 -18
- package/dist/cli/run-command.d.ts +12 -0
- package/dist/cli/run-command.js +42 -0
- package/dist/cli/run-resolver.d.ts +36 -0
- package/dist/commands/pipeline-command.js +3 -1
- package/dist/commands/ticket-command.d.ts +15 -0
- package/dist/commands/ticket-command.js +308 -0
- package/dist/hooks.d.ts +1 -1
- package/dist/install-commands/opencode.js +3 -2
- package/dist/moka-submit.d.ts +6 -6
- package/dist/pipeline-init.js +109 -1
- package/dist/runner-event-schema.d.ts +8 -8
- package/dist/runtime/services/backlog-service.d.ts +16 -0
- package/dist/runtime/services/backlog-service.js +25 -0
- package/dist/standard-output-schemas.js +2 -0
- package/dist/tickets/apply-ticket-plan.js +75 -0
- package/dist/tickets/backlog-task-store.js +130 -0
- package/dist/tickets/ticket-graph.js +92 -0
- package/dist/tickets/ticket-plan-command-args.js +28 -0
- package/dist/tickets/ticket-plan-render.js +22 -0
- package/dist/tickets/ticket-plan.js +92 -0
- package/dist/tickets/ticket-selection.js +74 -0
- package/dist/tickets/ticket-task-index.js +24 -0
- package/dist/tickets/validation-error-format.js +12 -0
- package/docs/mcp-gateway.md +10 -9
- package/docs/operator-guide.md +36 -0
- package/package.json +1 -1
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
|
|
@@ -180,9 +180,9 @@ mcp_backends:
|
|
|
180
180
|
required: true
|
|
181
181
|
credentials: [GITHUB_TOKEN]
|
|
182
182
|
role: repository, PR, issue, and release workflow access
|
|
183
|
-
- id: playwright
|
|
184
|
-
name: Playwright
|
|
185
|
-
locality:
|
|
183
|
+
- id: playwright
|
|
184
|
+
name: Playwright
|
|
185
|
+
locality: shared-remote
|
|
186
186
|
required: true
|
|
187
187
|
credentials: []
|
|
188
188
|
role: browser automation and frontend verification
|
package/defaults/profiles.yaml
CHANGED
|
@@ -13,6 +13,9 @@ mcp_gateway:
|
|
|
13
13
|
uidotsh:
|
|
14
14
|
locality: shared-remote
|
|
15
15
|
tool_prefixes: [uidotsh]
|
|
16
|
+
playwright:
|
|
17
|
+
locality: shared-remote
|
|
18
|
+
tool_prefixes: [playwright]
|
|
16
19
|
qdrant:
|
|
17
20
|
locality: repo-scoped-remote
|
|
18
21
|
tool_prefixes: [qdrant]
|
|
@@ -108,6 +111,20 @@ profiles:
|
|
|
108
111
|
format: json_schema
|
|
109
112
|
schema_path: .pipeline/schemas/research.schema.json
|
|
110
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 }
|
|
111
128
|
moka-inspector:
|
|
112
129
|
runner: opencode
|
|
113
130
|
model: openai/gpt-5.5-low
|
package/dist/backlog.js
ADDED
|
@@ -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 };
|
package/dist/cli/program.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/cli/program.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 };
|