diff --git a/src/actions/index.ts b/src/actions/index.ts
new file mode 100644
index 00000000..a4c88726
--- /dev/null
+++ b/src/actions/index.ts
@@ -0,0 +1,8 @@
+export { default as onStartup } from './onStartup'
+export { default as onTutorialConfig } from './onTutorialConfig'
+export { default as onTutorialContinueConfig } from './onTutorialContinueConfig'
+export { default as onValidateSetup } from './onValidateSetup'
+export { default as onRunReset } from './onRunReset'
+export { default as onErrorPage } from './onErrorPage'
+export { default as onTestPass } from './onTestPass'
+export { onSetupActions, onSolutionActions } from './onActions'
diff --git a/src/actions/setupActions.ts b/src/actions/onActions.ts
similarity index 85%
rename from src/actions/setupActions.ts
rename to src/actions/onActions.ts
index 21e74fb3..1e3d4a05 100644
--- a/src/actions/setupActions.ts
+++ b/src/actions/onActions.ts
@@ -13,7 +13,7 @@ interface SetupActions {
   dir?: string
 }
 
-export const setupActions = async ({ actions, send, dir }: SetupActions): Promise<void> => {
+export const onSetupActions = async ({ actions, send, dir }: SetupActions): Promise<void> => {
   if (!actions) {
     return
   }
@@ -49,7 +49,7 @@ export const setupActions = async ({ actions, send, dir }: SetupActions): Promis
   }
 }
 
-export const solutionActions = async (params: SetupActions): Promise<void> => {
+export const onSolutionActions = async (params: SetupActions): Promise<void> => {
   await git.clear()
-  return setupActions(params).catch(onError)
+  return onSetupActions(params).catch(onError)
 }
diff --git a/src/actions/onErrorPage.ts b/src/actions/onErrorPage.ts
new file mode 100644
index 00000000..dd4dc742
--- /dev/null
+++ b/src/actions/onErrorPage.ts
@@ -0,0 +1,26 @@
+import * as T from 'typings'
+import { readFile } from '../services/node'
+import logger from '../services/logger'
+
+const onErrorPage = async (action: T.Action) => {
+  // Error middleware
+  if (action?.payload?.error?.type) {
+    // load error markdown message
+    const error = action.payload.error
+    const errorMarkdown = await readFile(__dirname, '..', '..', 'errors', `${action.payload.error.type}.md`).catch(
+      () => {
+        // onError(new Error(`Error Markdown file not found for ${action.type}`))
+      },
+    )
+
+    // log error to console for safe keeping
+    logger(`ERROR:\n ${errorMarkdown}`)
+
+    if (errorMarkdown) {
+      // add a clearer error message for the user
+      error.message = `${errorMarkdown}\n\n${error.message}`
+    }
+  }
+}
+
+export default onErrorPage
diff --git a/src/actions/onRunReset.ts b/src/actions/onRunReset.ts
new file mode 100644
index 00000000..745c9123
--- /dev/null
+++ b/src/actions/onRunReset.ts
@@ -0,0 +1,32 @@
+import * as T from 'typings'
+import * as TT from 'typings/tutorial'
+import Context from '../services/context/context'
+import { exec } from '../services/node'
+import reset from '../services/reset'
+import getLastCommitHash from '../services/reset/lastHash'
+
+const onRunReset = async (context: Context) => {
+  // reset to timeline
+  const tutorial: TT.Tutorial | null = context.tutorial.get()
+  const position: T.Position = context.position.get()
+
+  // get last pass commit
+  const hash = getLastCommitHash(position, tutorial?.levels || [])
+
+  const branch = tutorial?.config.repo.branch
+
+  if (!branch) {
+    console.error('No repo branch found for tutorial')
+    return
+  }
+
+  // load timeline until last pass commit
+  reset({ branch, hash })
+
+  // if tutorial.config.reset.command, run it
+  if (tutorial?.config?.reset?.command) {
+    await exec({ command: tutorial.config.reset.command })
+  }
+}
+
+export default onRunReset
diff --git a/src/actions/onStartup.ts b/src/actions/onStartup.ts
new file mode 100644
index 00000000..2c3de424
--- /dev/null
+++ b/src/actions/onStartup.ts
@@ -0,0 +1,79 @@
+import * as vscode from 'vscode'
+import * as T from 'typings'
+import * as TT from 'typings/tutorial'
+import * as E from 'typings/error'
+import Context from '../services/context/context'
+import { WORKSPACE_ROOT, TUTORIAL_URL } from '../environment'
+import fetch from 'node-fetch'
+import logger from '../services/logger'
+
+const onStartup = async (
+  context: Context,
+  workspaceState: vscode.Memento,
+  send: (action: T.Action) => Promise<void>,
+) => {
+  try {
+    // check if a workspace is open, otherwise nothing works
+    const noActiveWorkspace = !WORKSPACE_ROOT.length
+    if (noActiveWorkspace) {
+      const error: E.ErrorMessage = {
+        type: 'NoWorkspaceFound',
+        message: '',
+        actions: [
+          {
+            label: 'Open Workspace',
+            transition: 'REQUEST_WORKSPACE',
+          },
+        ],
+      }
+      send({ type: 'NO_WORKSPACE', payload: { error } })
+      return
+    }
+
+    const env = {
+      machineId: vscode.env.machineId,
+      sessionId: vscode.env.sessionId,
+    }
+
+    // load tutorial from url
+    if (TUTORIAL_URL) {
+      try {
+        const tutorialRes = await fetch(TUTORIAL_URL)
+        const tutorial = await tutorialRes.json()
+        send({ type: 'START_TUTORIAL_FROM_URL', payload: { tutorial } })
+        return
+      } catch (e) {
+        console.log(`Failed to load tutorial from url ${TUTORIAL_URL} with error "${e.message}"`)
+      }
+    }
+
+    // continue from tutorial from local storage
+    const tutorial: TT.Tutorial | null = context.tutorial.get()
+
+    // no stored tutorial, must start new tutorial
+    if (!tutorial || !tutorial.id) {
+      send({ type: 'START_NEW_TUTORIAL', payload: { env } })
+      return
+    }
+
+    // load continued tutorial position & progress
+    const { position, progress } = await context.setTutorial(workspaceState, tutorial)
+    logger('CONTINUE STATE', position, progress)
+
+    if (progress.complete) {
+      // tutorial is already complete
+      send({ type: 'TUTORIAL_ALREADY_COMPLETE', payload: { env } })
+      return
+    }
+    // communicate to client the tutorial & stepProgress state
+    send({ type: 'LOAD_STORED_TUTORIAL', payload: { env, tutorial, progress, position } })
+  } catch (e) {
+    const error = {
+      type: 'UnknownError',
+      message: `Location: Editor startup\n\n${e.message}`,
+    }
+    send({ type: 'EDITOR_STARTUP_FAILED', payload: { error } })
+  }
+}
+
+export default onStartup
diff --git a/src/actions/onTestPass.ts b/src/actions/onTestPass.ts
new file mode 100644
index 00000000..0169a942
--- /dev/null
+++ b/src/actions/onTestPass.ts
@@ -0,0 +1,16 @@
+import * as git from '../services/git'
+import * as T from 'typings'
+import Context from '../services/context/context'
+
+const onTestPass = (action: T.Action, context: Context) => {
+  const tutorial = context.tutorial.get()
+  if (!tutorial) {
+    throw new Error('Error with current tutorial. Tutorial may be missing an id.')
+  }
+  // update local storage stepProgress
+  const progress = context.progress.setStepComplete(tutorial, action.payload.position.stepId)
+  context.position.setPositionFromProgress(tutorial, progress)
+  git.saveCommit('Save progress')
+}
+
+export default onTestPass
diff --git a/src/actions/onTutorialConfig.ts b/src/actions/onTutorialConfig.ts
new file mode 100644
index 00000000..7a1f7221
--- /dev/null
+++ b/src/actions/onTutorialConfig.ts
@@ -0,0 +1,121 @@
+import * as vscode from 'vscode'
+import * as T from 'typings'
+import * as TT from 'typings/tutorial'
+import * as E from 'typings/error'
+import { satisfies } from 'semver'
+import { onEvent } from '../services/telemetry'
+import { version, compareVersions } from '../services/dependencies'
+import Context from '../services/context/context'
+import tutorialConfig from './utils/tutorialConfig'
+
+const onTutorialConfig = async (action: T.Action, context: Context, workspaceState: vscode.Memento, send: any) => {
+  try {
+    const data: TT.Tutorial = action.payload.tutorial
+
+    onEvent('tutorial_start', {
+      tutorial_id: data.id,
+      tutorial_version: data.version,
+      tutorial_title: data.summary.title,
+    })
+
+    // validate extension version
+    const expectedAppVersion = data.config?.appVersions?.vscode
+    if (expectedAppVersion) {
+      const extension = vscode.extensions.getExtension('coderoad.coderoad')
+      if (extension) {
+        const currentAppVersion = extension.packageJSON.version
+        const satisfied = satisfies(currentAppVersion, expectedAppVersion)
+        if (!satisfied) {
+          const error: E.ErrorMessage = {
+            type: 'UnmetExtensionVersion',
+            message: `Expected CodeRoad v${expectedAppVersion}, but found v${currentAppVersion}`,
+          }
+          send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
+          return
+        }
+      }
+    }
+
+    // setup tutorial config (save watcher, test runner, etc)
+    await context.setTutorial(workspaceState, data)
+
+    // validate dependencies
+    const dependencies = data.config.dependencies
+    if (dependencies && dependencies.length) {
+      for (const dep of dependencies) {
+        // check dependency is installed
+        const currentVersion: string | null = await version(dep.name)
+        if (!currentVersion) {
+          // use a custom error message
+          const error: E.ErrorMessage = {
+            type: 'MissingTutorialDependency',
+            message: dep.message || `Process "${dep.name}" is required but not found. It may need to be installed`,
+            actions: [
+              {
+                label: 'Check Again',
+                transition: 'TRY_AGAIN',
+              },
+            ],
+          }
+          send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
+          return
+        }
+
+        // check dependency version
+        const satisfiedDependency = await compareVersions(currentVersion, dep.version)
+
+        if (!satisfiedDependency) {
+          const error: E.ErrorMessage = {
+            type: 'UnmetTutorialDependency',
+            message: `Expected ${dep.name} to have version ${dep.version}, but found version ${currentVersion}`,
+            actions: [
+              {
+                label: 'Check Again',
+                transition: 'TRY_AGAIN',
+              },
+            ],
+          }
+          send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
+          return
+        }
+
+        if (satisfiedDependency !== true) {
+          const error: E.ErrorMessage = satisfiedDependency || {
+            type: 'UnknownError',
+            message: `Something went wrong comparing dependency for ${name}`,
+            actions: [
+              {
+                label: 'Try Again',
+                transition: 'TRY_AGAIN',
+              },
+            ],
+          }
+          send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
+          return
+        }
+      }
+    }
+
+    const error: E.ErrorMessage | void = await tutorialConfig({ data }).catch((error: Error) => ({
+      type: 'UnknownError',
+      message: `Location: tutorial config.\n\n${error.message}`,
+    }))
+
+    // has error
+    if (error && error.type) {
+      send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
+      return
+    }
+
+    // report back to the webview that setup is complete
+    send({ type: 'TUTORIAL_CONFIGURED' })
+  } catch (e) {
+    const error = {
+      type: 'UnknownError',
+      message: `Location: EditorTutorialConfig.\n\n ${e.message}`,
+    }
+    send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
+  }
+}
+
+export default onTutorialConfig
diff --git a/src/actions/onTutorialContinueConfig.ts b/src/actions/onTutorialContinueConfig.ts
new file mode 100644
index 00000000..2610b14b
--- /dev/null
+++ b/src/actions/onTutorialContinueConfig.ts
@@ -0,0 +1,29 @@
+import * as vscode from 'vscode'
+import * as T from 'typings'
+import * as TT from 'typings/tutorial'
+import Context from '../services/context/context'
+import tutorialConfig from './utils/tutorialConfig'
+import { COMMANDS } from '../commands'
+
+const onTutorialContinueConfig = async (action: T.Action, context: Context, send: any) => {
+  try {
+    const tutorialContinue: TT.Tutorial | null = context.tutorial.get()
+    if (!tutorialContinue) {
+      throw new Error('Invalid tutorial to continue')
+    }
+    await tutorialConfig({
+      data: tutorialContinue,
+      alreadyConfigured: true,
+    })
+    // update the current stepId on startup
+    vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position)
+  } catch (e) {
+    const error = {
+      type: 'UnknownError',
+      message: `Location: Editor tutorial continue config.\n\n ${e.message}`,
+    }
+    send({ type: 'CONTINUE_FAILED', payload: { error } })
+  }
+}
+
+export default onTutorialContinueConfig
diff --git a/src/actions/onValidateSetup.ts b/src/actions/onValidateSetup.ts
new file mode 100644
index 00000000..90657e90
--- /dev/null
+++ b/src/actions/onValidateSetup.ts
@@ -0,0 +1,54 @@
+import * as E from 'typings/error'
+import { version } from '../services/dependencies'
+import { checkWorkspaceEmpty } from '../services/workspace'
+
+const onValidateSetup = async (send: any) => {
+  try {
+    // check workspace is selected
+    const isEmptyWorkspace = await checkWorkspaceEmpty()
+    if (!isEmptyWorkspace) {
+      const error: E.ErrorMessage = {
+        type: 'WorkspaceNotEmpty',
+        message: '',
+        actions: [
+          {
+            label: 'Open Workspace',
+            transition: 'REQUEST_WORKSPACE',
+          },
+          {
+            label: 'Check Again',
+            transition: 'RETRY',
+          },
+        ],
+      }
+      send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } })
+      return
+    }
+    // check Git is installed.
+    // Should wait for workspace before running otherwise requires access to root folder
+    const isGitInstalled = await version('git')
+    if (!isGitInstalled) {
+      const error: E.ErrorMessage = {
+        type: 'GitNotFound',
+        message: '',
+        actions: [
+          {
+            label: 'Check Again',
+            transition: 'RETRY',
+          },
+        ],
+      }
+      send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } })
+      return
+    }
+    send({ type: 'SETUP_VALIDATED' })
+  } catch (e) {
+    const error = {
+      type: 'UknownError',
+      message: e.message,
+    }
+    send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } })
+  }
+}
+
+export default onValidateSetup
diff --git a/src/actions/saveCommit.ts b/src/actions/saveCommit.ts
deleted file mode 100644
index 74002c5e..00000000
--- a/src/actions/saveCommit.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import * as git from '../services/git'
-
-async function saveCommit(): Promise<void> {
-  git.saveCommit('Save progress')
-}
-
-export default saveCommit
diff --git a/src/actions/utils/loadWatchers.ts b/src/actions/utils/loadWatchers.ts
index 9dab23ec..978a3ce2 100644
--- a/src/actions/utils/loadWatchers.ts
+++ b/src/actions/utils/loadWatchers.ts
@@ -1,6 +1,6 @@
 import * as chokidar from 'chokidar'
 import * as vscode from 'vscode'
-import { COMMANDS } from '../../editor/commands'
+import { COMMANDS } from '../../commands'
 import { WORKSPACE_ROOT } from '../../environment'
 
 // NOTE: vscode createFileWatcher doesn't seem to detect changes outside of vscode
diff --git a/src/actions/utils/openFiles.ts b/src/actions/utils/openFiles.ts
index 34580f04..35e97710 100644
--- a/src/actions/utils/openFiles.ts
+++ b/src/actions/utils/openFiles.ts
@@ -1,6 +1,6 @@
 import { join } from 'path'
 import * as vscode from 'vscode'
-import { COMMANDS } from '../../editor/commands'
+import { COMMANDS } from '../../commands'
 
 const openFiles = async (files: string[]) => {
   if (!files.length) {
diff --git a/src/actions/tutorialConfig.ts b/src/actions/utils/tutorialConfig.ts
similarity index 93%
rename from src/actions/tutorialConfig.ts
rename to src/actions/utils/tutorialConfig.ts
index d01510f3..a4ef01d9 100644
--- a/src/actions/tutorialConfig.ts
+++ b/src/actions/utils/tutorialConfig.ts
@@ -1,9 +1,9 @@
 import * as E from 'typings/error'
 import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
-import { COMMANDS } from '../editor/commands'
-import * as git from '../services/git'
-import { DISABLE_RUN_ON_SAVE } from '../environment'
+import { COMMANDS } from '../../commands'
+import * as git from '../../services/git'
+import { DISABLE_RUN_ON_SAVE } from '../../environment'
 
 interface TutorialConfigParams {
   data: TT.Tutorial
diff --git a/src/channel.ts b/src/channel.ts
new file mode 100644
index 00000000..1a7b116d
--- /dev/null
+++ b/src/channel.ts
@@ -0,0 +1,115 @@
+import * as T from 'typings'
+import * as vscode from 'vscode'
+import { setupActions, solutionActions } from './actions/onActions'
+import { COMMANDS } from './commands'
+import Context from './services/context/context'
+import logger from './services/logger'
+import { openWorkspace } from './services/workspace'
+import { showOutput } from './services/testRunner/output'
+import * as actions from './actions'
+
+interface Channel {
+  receive(action: T.Action): Promise<void>
+  send(action: T.Action): Promise<void>
+}
+
+interface ChannelProps {
+  postMessage: (action: T.Action) => Thenable<boolean>
+  workspaceState: vscode.Memento
+}
+
+class Channel implements Channel {
+  private postMessage: (action: T.Action) => Thenable<boolean>
+  private workspaceState: vscode.Memento
+  private context: Context
+  constructor({ postMessage, workspaceState }: ChannelProps) {
+    // workspaceState used for local storage
+    this.workspaceState = workspaceState
+    this.postMessage = postMessage
+    this.context = new Context(workspaceState)
+  }
+
+  // receive from webview
+  public receive = async (action: T.Action): Promise<void> => {
+    // action may be an object.type or plain string
+    const actionType: string = typeof action === 'string' ? action : action.type
+    // const onError = (error: T.ErrorMessage) => this.send({ type: 'ERROR', payload: { error } })
+
+    logger(`EXT RECEIVED: "${actionType}"`)
+
+    switch (actionType) {
+      case 'EDITOR_STARTUP':
+        actions.onStartup(this.context, this.workspaceState, this.send)
+        return
+      // clear tutorial local storage
+      case 'TUTORIAL_CLEAR':
+        // clear current progress/position/tutorial
+        this.context.reset()
+        return
+      // configure test runner, language, git
+      case 'EDITOR_TUTORIAL_CONFIG':
+        actions.onTutorialConfig(action, this.context, this.workspaceState, this.send)
+        return
+      case 'EDITOR_TUTORIAL_CONTINUE_CONFIG':
+        actions.onTutorialContinueConfig(action, this.context, this.send)
+        return
+      case 'EDITOR_VALIDATE_SETUP':
+        actions.onValidateSetup(this.send)
+        return
+      case 'EDITOR_REQUEST_WORKSPACE':
+        openWorkspace()
+        return
+      // load step actions (git commits, commands, open files)
+      case 'SETUP_ACTIONS':
+        await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position)
+        actions.onSetupActions({ actions: action.payload.actions, send: this.send })
+        return
+      // load solution step actions (git commits, commands, open files)
+      case 'SOLUTION_ACTIONS':
+        await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position)
+        await actions.onSolutionActions({ actions: action.payload.actions, send: this.send })
+        // run test following solution to update position
+        vscode.commands.executeCommand(COMMANDS.RUN_TEST)
+        return
+      case 'EDITOR_SYNC_PROGRESS':
+        // update progress when a level is deemed complete in the client
+        await this.context.progress.syncProgress(action.payload.progress)
+        return
+      case 'EDITOR_OPEN_LOGS':
+        const channel = action.payload.channel
+        await showOutput(channel)
+        return
+      case 'EDITOR_RUN_TEST':
+        vscode.commands.executeCommand(COMMANDS.RUN_TEST, action?.payload)
+        return
+      case 'EDITOR_RUN_RESET':
+        actions.onRunReset(this.context)
+        return
+      default:
+        logger(`No match for action type: ${actionType}`)
+        return
+    }
+  }
+  // send to webview
+  public send = async (action: T.Action): Promise<void> => {
+    // load error page if error action is triggered
+    actions.onErrorPage(action)
+    // action may be an object.type or plain string
+    const actionType: string = typeof action === 'string' ? action : action.type
+
+    logger(`EXT TO CLIENT: "${actionType}"`)
+
+    switch (actionType) {
+      case 'TEST_PASS':
+        actions.onTestPass(action, this.context)
+    }
+
+    // send message
+    const sentToClient = await this.postMessage(action)
+    if (!sentToClient) {
+      throw new Error(`Message post failure: ${JSON.stringify(action)}`)
+    }
+  }
+}
+
+export default Channel
diff --git a/src/channel/index.ts b/src/channel/index.ts
deleted file mode 100644
index 19cff9e4..00000000
--- a/src/channel/index.ts
+++ /dev/null
@@ -1,405 +0,0 @@
-import * as T from 'typings'
-import * as TT from 'typings/tutorial'
-import * as E from 'typings/error'
-import * as vscode from 'vscode'
-import fetch from 'node-fetch'
-import { satisfies } from 'semver'
-import saveCommit from '../actions/saveCommit'
-import { setupActions, solutionActions } from '../actions/setupActions'
-import tutorialConfig from '../actions/tutorialConfig'
-import { COMMANDS } from '../editor/commands'
-import Context from './context'
-import { readFile } from 'fs'
-import { join } from 'path'
-import { promisify } from 'util'
-import logger from '../services/logger'
-import { version, compareVersions } from '../services/dependencies'
-import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace'
-import { showOutput } from '../services/testRunner/output'
-import { exec } from '../services/node'
-import { WORKSPACE_ROOT, TUTORIAL_URL } from '../environment'
-import reset from '../services/reset'
-import getLastCommitHash from '../services/reset/lastHash'
-import { onEvent } from '../services/telemetry'
-
-const readFileAsync = promisify(readFile)
-
-interface Channel {
-  receive(action: T.Action): Promise<void>
-  send(action: T.Action): Promise<void>
-}
-
-interface ChannelProps {
-  postMessage: (action: T.Action) => Thenable<boolean>
-  workspaceState: vscode.Memento
-}
-
-class Channel implements Channel {
-  private postMessage: (action: T.Action) => Thenable<boolean>
-  private workspaceState: vscode.Memento
-  private context: Context
-  constructor({ postMessage, workspaceState }: ChannelProps) {
-    // workspaceState used for local storage
-    this.workspaceState = workspaceState
-    this.postMessage = postMessage
-    this.context = new Context(workspaceState)
-  }
-
-  // receive from webview
-  public receive = async (action: T.Action): Promise<void> => {
-    // action may be an object.type or plain string
-    const actionType: string = typeof action === 'string' ? action : action.type
-    // const onError = (error: T.ErrorMessage) => this.send({ type: 'ERROR', payload: { error } })
-
-    logger(`EXT RECEIVED: "${actionType}"`)
-
-    switch (actionType) {
-      case 'EDITOR_STARTUP':
-        try {
-          // check if a workspace is open, otherwise nothing works
-          const noActiveWorkspace = !WORKSPACE_ROOT.length
-          if (noActiveWorkspace) {
-            const error: E.ErrorMessage = {
-              type: 'NoWorkspaceFound',
-              message: '',
-              actions: [
-                {
-                  label: 'Open Workspace',
-                  transition: 'REQUEST_WORKSPACE',
-                },
-              ],
-            }
-            this.send({ type: 'NO_WORKSPACE', payload: { error } })
-            return
-          }
-
-          const env = {
-            machineId: vscode.env.machineId,
-            sessionId: vscode.env.sessionId,
-          }
-
-          // load tutorial from url
-          if (TUTORIAL_URL) {
-            try {
-              const tutorialRes = await fetch(TUTORIAL_URL)
-              const tutorial = await tutorialRes.json()
-              this.send({ type: 'START_TUTORIAL_FROM_URL', payload: { tutorial } })
-              return
-            } catch (e) {
-              console.log(`Failed to load tutorial from url ${TUTORIAL_URL} with error "${e.message}"`)
-            }
-          }
-
-          // continue from tutorial from local storage
-          const tutorial: TT.Tutorial | null = this.context.tutorial.get()
-
-          // no stored tutorial, must start new tutorial
-          if (!tutorial || !tutorial.id) {
-            this.send({ type: 'START_NEW_TUTORIAL', payload: { env } })
-            return
-          }
-
-          // load continued tutorial position & progress
-          const { position, progress } = await this.context.setTutorial(this.workspaceState, tutorial)
-          logger('CONTINUE STATE', position, progress)
-
-          if (progress.complete) {
-            // tutorial is already complete
-            this.send({ type: 'TUTORIAL_ALREADY_COMPLETE', payload: { env } })
-            return
-          }
-          // communicate to client the tutorial & stepProgress state
-          this.send({ type: 'LOAD_STORED_TUTORIAL', payload: { env, tutorial, progress, position } })
-        } catch (e) {
-          const error = {
-            type: 'UnknownError',
-            message: `Location: Editor startup\n\n${e.message}`,
-          }
-          this.send({ type: 'EDITOR_STARTUP_FAILED', payload: { error } })
-        }
-        return
-
-      // clear tutorial local storage
-      case 'TUTORIAL_CLEAR':
-        // clear current progress/position/tutorial
-        this.context.reset()
-        return
-      // configure test runner, language, git
-      case 'EDITOR_TUTORIAL_CONFIG':
-        try {
-          const data: TT.Tutorial = action.payload.tutorial
-
-          onEvent('tutorial_start', {
-            tutorial_id: data.id,
-            tutorial_version: data.version,
-            tutorial_title: data.summary.title,
-          })
-
-          // validate extension version
-          const expectedAppVersion = data.config?.appVersions?.vscode
-          if (expectedAppVersion) {
-            const extension = vscode.extensions.getExtension('coderoad.coderoad')
-            if (extension) {
-              const currentAppVersion = extension.packageJSON.version
-              const satisfied = satisfies(currentAppVersion, expectedAppVersion)
-              if (!satisfied) {
-                const error: E.ErrorMessage = {
-                  type: 'UnmetExtensionVersion',
-                  message: `Expected CodeRoad v${expectedAppVersion}, but found v${currentAppVersion}`,
-                }
-                this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
-                return
-              }
-            }
-          }
-
-          // setup tutorial config (save watcher, test runner, etc)
-          await this.context.setTutorial(this.workspaceState, data)
-
-          // validate dependencies
-          const dependencies = data.config.dependencies
-          if (dependencies && dependencies.length) {
-            for (const dep of dependencies) {
-              // check dependency is installed
-              const currentVersion: string | null = await version(dep.name)
-              if (!currentVersion) {
-                // use a custom error message
-                const error: E.ErrorMessage = {
-                  type: 'MissingTutorialDependency',
-                  message:
-                    dep.message || `Process "${dep.name}" is required but not found. It may need to be installed`,
-                  actions: [
-                    {
-                      label: 'Check Again',
-                      transition: 'TRY_AGAIN',
-                    },
-                  ],
-                }
-                this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
-                return
-              }
-
-              // check dependency version
-              const satisfiedDependency = await compareVersions(currentVersion, dep.version)
-
-              if (!satisfiedDependency) {
-                const error: E.ErrorMessage = {
-                  type: 'UnmetTutorialDependency',
-                  message: `Expected ${dep.name} to have version ${dep.version}, but found version ${currentVersion}`,
-                  actions: [
-                    {
-                      label: 'Check Again',
-                      transition: 'TRY_AGAIN',
-                    },
-                  ],
-                }
-                this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
-                return
-              }
-
-              if (satisfiedDependency !== true) {
-                const error: E.ErrorMessage = satisfiedDependency || {
-                  type: 'UnknownError',
-                  message: `Something went wrong comparing dependency for ${name}`,
-                  actions: [
-                    {
-                      label: 'Try Again',
-                      transition: 'TRY_AGAIN',
-                    },
-                  ],
-                }
-                this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
-                return
-              }
-            }
-          }
-
-          const error: E.ErrorMessage | void = await tutorialConfig({ data }).catch((error: Error) => ({
-            type: 'UnknownError',
-            message: `Location: tutorial config.\n\n${error.message}`,
-          }))
-
-          // has error
-          if (error && error.type) {
-            this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
-            return
-          }
-
-          // report back to the webview that setup is complete
-          this.send({ type: 'TUTORIAL_CONFIGURED' })
-        } catch (e) {
-          const error = {
-            type: 'UnknownError',
-            message: `Location: EditorTutorialConfig.\n\n ${e.message}`,
-          }
-          this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
-        }
-        return
-      case 'EDITOR_TUTORIAL_CONTINUE_CONFIG':
-        try {
-          const tutorialContinue: TT.Tutorial | null = this.context.tutorial.get()
-          if (!tutorialContinue) {
-            throw new Error('Invalid tutorial to continue')
-          }
-          await tutorialConfig({
-            data: tutorialContinue,
-            alreadyConfigured: true,
-          })
-          // update the current stepId on startup
-          vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position)
-        } catch (e) {
-          const error = {
-            type: 'UnknownError',
-            message: `Location: Editor tutorial continue config.\n\n ${e.message}`,
-          }
-          this.send({ type: 'CONTINUE_FAILED', payload: { error } })
-        }
-        return
-      case 'EDITOR_VALIDATE_SETUP':
-        try {
-          // check workspace is selected
-          const isEmptyWorkspace = await checkWorkspaceEmpty()
-          if (!isEmptyWorkspace) {
-            const error: E.ErrorMessage = {
-              type: 'WorkspaceNotEmpty',
-              message: '',
-              actions: [
-                {
-                  label: 'Open Workspace',
-                  transition: 'REQUEST_WORKSPACE',
-                },
-                {
-                  label: 'Check Again',
-                  transition: 'RETRY',
-                },
-              ],
-            }
-            this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } })
-            return
-          }
-          // check Git is installed.
-          // Should wait for workspace before running otherwise requires access to root folder
-          const isGitInstalled = await version('git')
-          if (!isGitInstalled) {
-            const error: E.ErrorMessage = {
-              type: 'GitNotFound',
-              message: '',
-              actions: [
-                {
-                  label: 'Check Again',
-                  transition: 'RETRY',
-                },
-              ],
-            }
-            this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } })
-            return
-          }
-          this.send({ type: 'SETUP_VALIDATED' })
-        } catch (e) {
-          const error = {
-            type: 'UknownError',
-            message: e.message,
-          }
-          this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } })
-        }
-        return
-      case 'EDITOR_REQUEST_WORKSPACE':
-        openWorkspace()
-        return
-      // load step actions (git commits, commands, open files)
-      case 'SETUP_ACTIONS':
-        await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position)
-        setupActions({ actions: action.payload.actions, send: this.send })
-        return
-      // load solution step actions (git commits, commands, open files)
-      case 'SOLUTION_ACTIONS':
-        await vscode.commands.executeCommand(COMMANDS.SET_CURRENT_POSITION, action.payload.position)
-        await solutionActions({ actions: action.payload.actions, send: this.send })
-        // run test following solution to update position
-        vscode.commands.executeCommand(COMMANDS.RUN_TEST)
-        return
-      case 'EDITOR_SYNC_PROGRESS':
-        // update progress when a level is deemed complete in the client
-        await this.context.progress.syncProgress(action.payload.progress)
-        return
-      case 'EDITOR_OPEN_LOGS':
-        const channel = action.payload.channel
-        await showOutput(channel)
-        return
-      case 'EDITOR_RUN_TEST':
-        vscode.commands.executeCommand(COMMANDS.RUN_TEST, action?.payload)
-        return
-      case 'EDITOR_RUN_RESET':
-        // reset to timeline
-        const tutorial: TT.Tutorial | null = this.context.tutorial.get()
-        const position: T.Position = this.context.position.get()
-
-        // get last pass commit
-        const hash = getLastCommitHash(position, tutorial?.levels || [])
-
-        const branch = tutorial?.config.repo.branch
-
-        if (!branch) {
-          console.error('No repo branch found for tutorial')
-          return
-        }
-
-        // load timeline until last pass commit
-        reset({ branch, hash })
-
-        // if tutorial.config.reset.command, run it
-        if (tutorial?.config?.reset?.command) {
-          await exec({ command: tutorial.config.reset.command })
-        }
-        return
-      default:
-        logger(`No match for action type: ${actionType}`)
-        return
-    }
-  }
-  // send to webview
-  public send = async (action: T.Action): Promise<void> => {
-    // Error middleware
-    if (action?.payload?.error?.type) {
-      // load error markdown message
-      const error = action.payload.error
-      const errorMarkdownFile = join(__dirname, '..', '..', 'errors', `${action.payload.error.type}.md`)
-      const errorMarkdown = await readFileAsync(errorMarkdownFile).catch(() => {
-        // onError(new Error(`Error Markdown file not found for ${action.type}`))
-      })
-
-      // log error to console for safe keeping
-      logger(`ERROR:\n ${errorMarkdown}`)
-
-      if (errorMarkdown) {
-        // add a clearer error message for the user
-        error.message = `${errorMarkdown}\n\n${error.message}`
-      }
-    }
-
-    // action may be an object.type or plain string
-    const actionType: string = typeof action === 'string' ? action : action.type
-
-    logger(`EXT TO CLIENT: "${actionType}"`)
-
-    switch (actionType) {
-      case 'TEST_PASS':
-        const tutorial = this.context.tutorial.get()
-        if (!tutorial) {
-          throw new Error('Error with current tutorial. Tutorial may be missing an id.')
-        }
-        // update local storage stepProgress
-        const progress = this.context.progress.setStepComplete(tutorial, action.payload.position.stepId)
-        this.context.position.setPositionFromProgress(tutorial, progress)
-        saveCommit()
-    }
-
-    // send message
-    const sentToClient = await this.postMessage(action)
-    if (!sentToClient) {
-      throw new Error(`Message post failure: ${JSON.stringify(action)}`)
-    }
-  }
-}
-
-export default Channel
diff --git a/src/editor/commands.ts b/src/commands.ts
similarity index 94%
rename from src/editor/commands.ts
rename to src/commands.ts
index a7ab6915..6b36ec62 100644
--- a/src/editor/commands.ts
+++ b/src/commands.ts
@@ -1,10 +1,10 @@
 import * as T from 'typings'
 import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
-import createTestRunner from '../services/testRunner'
-import { setupActions } from '../actions/setupActions'
-import createWebView from '../services/webview'
-import logger from '../services/logger'
+import createTestRunner from './services/testRunner'
+import { onSetupActions } from './actions/onActions'
+import createWebView from './services/webview'
+import logger from './services/logger'
 
 export const COMMANDS = {
   START: 'coderoad.start',
@@ -57,7 +57,7 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP
       if (setup) {
         // setup tutorial test runner commits
         // assumes git already exists
-        await setupActions({
+        await onSetupActions({
           actions: setup,
           send: webview.send,
           dir: testRunnerConfig.directory || testRunnerConfig.path,
diff --git a/src/editor/index.ts b/src/editor/index.ts
deleted file mode 100644
index bf900b5d..00000000
--- a/src/editor/index.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import * as vscode from 'vscode'
-import { createCommands } from './commands'
-import * as telemetry from '../services/telemetry'
-
-class Editor {
-  // extension context set on activation
-  // @ts-ignore
-  private vscodeExt: vscode.ExtensionContext
-
-  public activate = (vscodeExt: vscode.ExtensionContext): void => {
-    this.vscodeExt = vscodeExt
-
-    // set out 60/40 layout
-    vscode.commands.executeCommand('vscode.setEditorLayout', {
-      orientation: 0,
-      groups: [{ size: 0.6 }, { size: 0.4 }],
-    })
-
-    // commands
-    const commands = createCommands({
-      extensionPath: this.vscodeExt.extensionPath,
-      // NOTE: local storage must be bound to the vscodeExt.workspaceState
-      workspaceState: this.vscodeExt.workspaceState,
-    })
-
-    const subscribe = (sub: any) => {
-      this.vscodeExt.subscriptions.push(sub)
-    }
-
-    // register commands
-    for (const cmd in commands) {
-      const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd])
-      subscribe(command)
-    }
-
-    telemetry.activate(subscribe)
-  }
-  public deactivate = (): void => {
-    // cleanup subscriptions/tasks
-    for (const disposable of this.vscodeExt.subscriptions) {
-      disposable.dispose()
-    }
-
-    telemetry.deactivate()
-  }
-}
-
-export default Editor
diff --git a/src/extension.ts b/src/extension.ts
index a12e7e2d..49a65006 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,10 +1,46 @@
-import Editor from './editor'
+import * as vscode from 'vscode'
+import { createCommands } from './commands'
+import * as telemetry from './services/telemetry'
 
-// vscode editor
-export const editor = new Editor()
+let onDeactivate = () => {}
 
 // activate run on vscode extension initialization
-export const activate = editor.activate
+export const activate = (vscodeExt: vscode.ExtensionContext): void => {
+  // set out default 60/40 layout
+  vscode.commands.executeCommand('vscode.setEditorLayout', {
+    orientation: 0,
+    groups: [{ size: 0.6 }, { size: 0.4 }],
+  })
+
+  // commands
+  const commands = createCommands({
+    extensionPath: vscodeExt.extensionPath,
+    // NOTE: local storage must be bound to the vscodeExt.workspaceState
+    workspaceState: vscodeExt.workspaceState,
+  })
+
+  const subscribe = (sub: any) => {
+    vscodeExt.subscriptions.push(sub)
+  }
+
+  // register commands
+  for (const cmd in commands) {
+    const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd])
+    subscribe(command)
+  }
+
+  telemetry.activate(subscribe)
+
+  onDeactivate = () => {
+    // cleanup subscriptions/tasks
+    // handled within activate because it requires access to subscriptions
+    for (const disposable of vscodeExt.subscriptions) {
+      disposable.dispose()
+    }
+
+    telemetry.deactivate()
+  }
+}
 
 // deactivate run on vscode extension shut down
-export const deactivate = editor.deactivate
+export const deactivate = (): void => onDeactivate()
diff --git a/src/channel/context.ts b/src/services/context/context.ts
similarity index 100%
rename from src/channel/context.ts
rename to src/services/context/context.ts
diff --git a/src/channel/state/Position.ts b/src/services/context/state/Position.ts
similarity index 100%
rename from src/channel/state/Position.ts
rename to src/services/context/state/Position.ts
diff --git a/src/channel/state/Progress.ts b/src/services/context/state/Progress.ts
similarity index 97%
rename from src/channel/state/Progress.ts
rename to src/services/context/state/Progress.ts
index 691d52bf..8bd2daa8 100644
--- a/src/channel/state/Progress.ts
+++ b/src/services/context/state/Progress.ts
@@ -1,7 +1,7 @@
 import * as T from 'typings'
 import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
-import Storage from '../../services/storage'
+import Storage from '../../storage'
 
 const defaultValue: T.Progress = {
   levels: {},
diff --git a/src/channel/state/Tutorial.ts b/src/services/context/state/Tutorial.ts
similarity index 94%
rename from src/channel/state/Tutorial.ts
rename to src/services/context/state/Tutorial.ts
index 3a91d19c..4f0ccb55 100644
--- a/src/channel/state/Tutorial.ts
+++ b/src/services/context/state/Tutorial.ts
@@ -1,6 +1,6 @@
 import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
-import Storage from '../../services/storage'
+import Storage from '../../storage'
 
 // Tutorial
 class Tutorial {
diff --git a/src/services/node/index.ts b/src/services/node/index.ts
index 7026c7a6..a90dd208 100644
--- a/src/services/node/index.ts
+++ b/src/services/node/index.ts
@@ -6,6 +6,7 @@ import { WORKSPACE_ROOT } from '../../environment'
 
 const asyncExec = promisify(cpExec)
 const asyncRemoveFile = promisify(fs.unlink)
+const asyncReadFile = promisify(fs.readFile)
 
 interface ExecParams {
   command: string
@@ -24,3 +25,7 @@ export const exists = (...paths: string[]): boolean | never => {
 export const removeFile = (...paths: string[]) => {
   return asyncRemoveFile(join(WORKSPACE_ROOT, ...paths))
 }
+
+export const readFile = (...paths: string[]) => {
+  return asyncReadFile(join(...paths))
+}