Merge "v.io/jiri/util: use runutil.Sequence."
diff --git a/cl.go b/cl.go
index 9460338..7ad11d6 100644
--- a/cl.go
+++ b/cl.go
@@ -825,7 +825,7 @@
 			return err
 		}
 	}
-	if err := gerrit.Push(review.jirix.Run(), review.CLOpts); err != nil {
+	if err := gerrit.Push(review.jirix.NewSeq(), review.CLOpts); err != nil {
 		return gerritError(err.Error())
 	}
 	return nil
diff --git a/contrib.go b/contrib.go
index 066850a..5c4897b 100644
--- a/contrib.go
+++ b/contrib.go
@@ -126,7 +126,7 @@
 }
 
 func runContributors(jirix *jiri.X, args []string) error {
-	projects, err := project.LocalProjects(jirix, project.FastScan)
+	localProjects, err := project.LocalProjects(jirix, project.FastScan)
 	if err != nil {
 		return err
 	}
@@ -134,8 +134,8 @@
 	if len(args) != 0 {
 		projectNames = set.String.FromSlice(args)
 	} else {
-		for name, _ := range projects {
-			projectNames[name] = struct{}{}
+		for _, p := range localProjects {
+			projectNames[p.Name] = struct{}{}
 		}
 	}
 
@@ -145,41 +145,43 @@
 	}
 	contributors := map[string]*contributor{}
 	for name, _ := range projectNames {
-		project, ok := projects[name]
-		if !ok {
+		projects := localProjects.Find(name)
+		if len(projects) == 0 {
 			continue
 		}
-		if err := jirix.Run().Chdir(project.Path); err != nil {
-			return err
-		}
-		switch project.Protocol {
-		case "git":
-			lines, err := listCommitters(jirix)
-			if err != nil {
+		for _, project := range projects {
+			if err := jirix.Run().Chdir(project.Path); err != nil {
 				return err
 			}
-			for _, line := range lines {
-				matches := contributorRE.FindStringSubmatch(line)
-				if got, want := len(matches), 4; got != want {
-					return fmt.Errorf("unexpected length of %v: got %v, want %v", matches, got, want)
-				}
-				count, err := strconv.Atoi(strings.TrimSpace(matches[1]))
+			switch project.Protocol {
+			case "git":
+				lines, err := listCommitters(jirix)
 				if err != nil {
-					return fmt.Errorf("Atoi(%v) failed: %v", strings.TrimSpace(matches[1]), err)
+					return err
 				}
-				c := &contributor{
-					count: count,
-					email: strings.TrimSpace(matches[3]),
-					name:  strings.TrimSpace(matches[2]),
-				}
-				if c.email == "jenkins.veyron@gmail.com" || c.email == "jenkins.veyron.rw@gmail.com" {
-					continue
-				}
-				c.email, c.name = canonicalize(aliases, c.email, c.name)
-				if existing, ok := contributors[c.name]; ok {
-					existing.count += c.count
-				} else {
-					contributors[c.name] = c
+				for _, line := range lines {
+					matches := contributorRE.FindStringSubmatch(line)
+					if got, want := len(matches), 4; got != want {
+						return fmt.Errorf("unexpected length of %v: got %v, want %v", matches, got, want)
+					}
+					count, err := strconv.Atoi(strings.TrimSpace(matches[1]))
+					if err != nil {
+						return fmt.Errorf("Atoi(%v) failed: %v", strings.TrimSpace(matches[1]), err)
+					}
+					c := &contributor{
+						count: count,
+						email: strings.TrimSpace(matches[3]),
+						name:  strings.TrimSpace(matches[2]),
+					}
+					if c.email == "jenkins.veyron@gmail.com" || c.email == "jenkins.veyron.rw@gmail.com" {
+						continue
+					}
+					c.email, c.name = canonicalize(aliases, c.email, c.name)
+					if existing, ok := contributors[c.name]; ok {
+						existing.count += c.count
+					} else {
+						contributors[c.name] = c
+					}
 				}
 			}
 		}
diff --git a/gerrit/.api b/gerrit/.api
new file mode 100644
index 0000000..3ab044a
--- /dev/null
+++ b/gerrit/.api
@@ -0,0 +1,63 @@
+pkg gerrit, const PresubmitTestTypeAll PresubmitTestType
+pkg gerrit, const PresubmitTestTypeNone PresubmitTestType
+pkg gerrit, func New(*runutil.Sequence, string) *Gerrit
+pkg gerrit, func PresubmitTestTypes() []string
+pkg gerrit, func Push(*runutil.Sequence, CLOpts) error
+pkg gerrit, func Reference(CLOpts) string
+pkg gerrit, method (*Gerrit) PostReview(string, string, map[string]string) error
+pkg gerrit, method (*Gerrit) Query(string) ([]Change, error)
+pkg gerrit, method (*Gerrit) SetTopic(string, CLOpts) error
+pkg gerrit, method (*Gerrit) Submit(string) error
+pkg gerrit, method (Change) OwnerEmail() string
+pkg gerrit, method (Change) Reference() string
+pkg gerrit, type CLOpts struct
+pkg gerrit, type CLOpts struct, Autosubmit bool
+pkg gerrit, type CLOpts struct, Branch string
+pkg gerrit, type CLOpts struct, Ccs []string
+pkg gerrit, type CLOpts struct, Draft bool
+pkg gerrit, type CLOpts struct, Edit bool
+pkg gerrit, type CLOpts struct, Host string
+pkg gerrit, type CLOpts struct, Presubmit PresubmitTestType
+pkg gerrit, type CLOpts struct, Remote string
+pkg gerrit, type CLOpts struct, RemoteBranch string
+pkg gerrit, type CLOpts struct, Reviewers []string
+pkg gerrit, type CLOpts struct, Topic string
+pkg gerrit, type CLOpts struct, Verify bool
+pkg gerrit, type Change struct
+pkg gerrit, type Change struct, AutoSubmit bool
+pkg gerrit, type Change struct, Change_id string
+pkg gerrit, type Change struct, Current_revision string
+pkg gerrit, type Change struct, Labels map[string]map[string]interface{}
+pkg gerrit, type Change struct, MultiPart *MultiPartCLInfo
+pkg gerrit, type Change struct, Owner Owner
+pkg gerrit, type Change struct, PresubmitTest PresubmitTestType
+pkg gerrit, type Change struct, Project string
+pkg gerrit, type Change struct, Revisions Revisions
+pkg gerrit, type Change struct, Topic string
+pkg gerrit, type Comment struct
+pkg gerrit, type Comment struct, Line int
+pkg gerrit, type Comment struct, Message string
+pkg gerrit, type Commit struct
+pkg gerrit, type Commit struct, Message string
+pkg gerrit, type Fetch struct
+pkg gerrit, type Fetch struct, embedded Http
+pkg gerrit, type Gerrit struct
+pkg gerrit, type Http struct
+pkg gerrit, type Http struct, Ref string
+pkg gerrit, type MultiPartCLInfo struct
+pkg gerrit, type MultiPartCLInfo struct, Index int
+pkg gerrit, type MultiPartCLInfo struct, Topic string
+pkg gerrit, type MultiPartCLInfo struct, Total int
+pkg gerrit, type Owner struct
+pkg gerrit, type Owner struct, Email string
+pkg gerrit, type PresubmitTestType string
+pkg gerrit, type Review struct
+pkg gerrit, type Review struct, Comments map[string][]Comment
+pkg gerrit, type Review struct, Labels map[string]string
+pkg gerrit, type Review struct, Message string
+pkg gerrit, type Revision struct
+pkg gerrit, type Revision struct, embedded Commit
+pkg gerrit, type Revision struct, embedded Fetch
+pkg gerrit, type Revisions map[string]Revision
+pkg gerrit, type Topic struct
+pkg gerrit, type Topic struct, Topic string
diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go
index 558df3f..6aee750 100644
--- a/gerrit/gerrit.go
+++ b/gerrit/gerrit.go
@@ -80,20 +80,20 @@
 // Gerrit records a hostname of a Gerrit instance.
 type Gerrit struct {
 	host string
-	r    *runutil.Run
+	s    *runutil.Sequence
 }
 
 // New is the Gerrit factory.
-func New(r *runutil.Run, host string) *Gerrit {
+func New(s *runutil.Sequence, host string) *Gerrit {
 	return &Gerrit{
 		host: host,
-		r:    r,
+		s:    s,
 	}
 }
 
 // PostReview posts a review to the given Gerrit reference.
 func (g *Gerrit) PostReview(ref string, message string, labels map[string]string) (e error) {
-	cred, err := hostCredentials(g.r, g.host)
+	cred, err := hostCredentials(g.s, g.host)
 	if err != nil {
 		return err
 	}
@@ -144,7 +144,7 @@
 
 // SetTopic sets the topic of the given Gerrit reference.
 func (g *Gerrit) SetTopic(cl string, opts CLOpts) (e error) {
-	cred, err := hostCredentials(g.r, g.host)
+	cred, err := hostCredentials(g.s, g.host)
 	if err != nil {
 		return err
 	}
@@ -317,7 +317,7 @@
 // - https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
 // - https://gerrit-review.googlesource.com/Documentation/user-search.html
 func (g *Gerrit) Query(query string) (_ []Change, e error) {
-	cred, err := hostCredentials(g.r, g.host)
+	cred, err := hostCredentials(g.s, g.host)
 	if err != nil {
 		return nil, err
 	}
@@ -345,7 +345,7 @@
 
 // Submit submits the given changelist through Gerrit.
 func (g *Gerrit) Submit(changeID string) (e error) {
-	cred, err := hostCredentials(g.r, g.host)
+	cred, err := hostCredentials(g.s, g.host)
 	if err != nil {
 		return err
 	}
@@ -427,13 +427,10 @@
 
 // getRemoteURL returns the URL of the Gerrit project with respect to the
 // project identified by the current working directory.
-func getRemoteURL(run *runutil.Run, clOpts CLOpts) (string, error) {
+func getRemoteURL(seq *runutil.Sequence, clOpts CLOpts) (string, error) {
 	args := []string{"config", "--get", "remote.origin.url"}
 	var stdout, stderr bytes.Buffer
-	opts := run.Opts()
-	opts.Stdout = &stdout
-	opts.Stderr = &stderr
-	if err := run.CommandWithOpts(opts, "git", args...); err != nil {
+	if err := seq.Capture(&stdout, &stderr).Last("git", args...); err != nil {
 		return "", gitutil.Error(stdout.String(), stderr.String(), args...)
 	}
 	baseUrl := clOpts.Host
@@ -444,11 +441,11 @@
 }
 
 // Push pushes the current branch to Gerrit.
-func Push(run *runutil.Run, clOpts CLOpts) error {
+func Push(seq *runutil.Sequence, clOpts CLOpts) error {
 	remote := clOpts.Remote
 	if remote == "" {
 		var err error
-		remote, err = getRemoteURL(run, clOpts)
+		remote, err = getRemoteURL(seq, clOpts)
 		if err != nil {
 			return err
 		}
@@ -464,10 +461,7 @@
 		args = append(args, "--no-verify")
 	}
 	var stdout, stderr bytes.Buffer
-	opts := run.Opts()
-	opts.Stdout = &stdout
-	opts.Stderr = &stderr
-	if err := run.CommandWithOpts(opts, "git", args...); err != nil {
+	if err := seq.Capture(&stdout, &stderr).Last("git", args...); err != nil {
 		return gitutil.Error(stdout.String(), stderr.String(), args...)
 	}
 	for _, line := range strings.Split(stderr.String(), "\n") {
@@ -486,7 +480,7 @@
 // hostCredentials returns credentials for the given Gerrit host. The
 // function uses best effort to scan common locations where the
 // credentials could exist.
-func hostCredentials(run *runutil.Run, host string) (_ *credentials, e error) {
+func hostCredentials(seq *runutil.Sequence, host string) (_ *credentials, e error) {
 	// Check the host URL is valid.
 	url, err := url.Parse(host)
 	if err != nil {
@@ -498,7 +492,7 @@
 
 	// Look for the host credentials in the .netrc file.
 	netrcPath := filepath.Join(os.Getenv("HOME"), ".netrc")
-	file, err := run.Open(netrcPath)
+	file, err := seq.Open(netrcPath)
 	if err != nil {
 		if !os.IsNotExist(err) {
 			return nil, err
@@ -518,12 +512,9 @@
 	// Look for the host credentials in the git cookie file.
 	args := []string{"config", "--get", "http.cookiefile"}
 	var stdout, stderr bytes.Buffer
-	opts := run.Opts()
-	opts.Stdout = &stdout
-	opts.Stderr = &stderr
-	if err := run.CommandWithOpts(opts, "git", args...); err == nil {
+	if err := seq.Capture(&stdout, &stderr).Last("git", args...); err == nil {
 		cookieFilePath := strings.TrimSpace(stdout.String())
-		file, err := run.Open(cookieFilePath)
+		file, err := seq.Open(cookieFilePath)
 		if err != nil {
 			if !os.IsNotExist(err) {
 				return nil, err
diff --git a/profiles/commandline/driver.go b/profiles/commandline/driver.go
index 9277eb0..2e3679c 100644
--- a/profiles/commandline/driver.go
+++ b/profiles/commandline/driver.go
@@ -195,7 +195,7 @@
 
 func runList(jirix *jiri.X, args []string) error {
 	if showManifestFlag {
-		data, err := jirix.Run().ReadFile(manifestFlag)
+		data, err := jirix.NewSeq().ReadFile(manifestFlag)
 		if err != nil {
 			return err
 		}
diff --git a/profiles/env.go b/profiles/env.go
index a5788ea..7d5325b 100644
--- a/profiles/env.go
+++ b/profiles/env.go
@@ -243,7 +243,7 @@
 			// account for Go workspaces that span multiple jiri projects,
 			// such as: $JIRI_ROOT/release/go.
 			if strings.HasPrefix(absWorkspace, project.Path) || strings.HasPrefix(project.Path, absWorkspace) {
-				if _, err := jirix.Run().Stat(filepath.Join(absWorkspace)); err == nil {
+				if _, err := jirix.NewSeq().Stat(filepath.Join(absWorkspace)); err == nil {
 					path = append(path, absWorkspace)
 					break
 				}
diff --git a/profiles/env_test.go b/profiles/env_test.go
index 1280186..501368f 100644
--- a/profiles/env_test.go
+++ b/profiles/env_test.go
@@ -181,7 +181,7 @@
 	case "VDLPATH":
 		// Make a fake src directory.
 		want = filepath.Join(fake.X.Root, "test", "src")
-		if err := fake.X.Run().MkdirAll(want, 0755); err != nil {
+		if err := fake.X.NewSeq().MkdirAll(want, 0755).Done(); err != nil {
 			t.Fatalf("%v", err)
 		}
 		want = "VDLPATH=" + want
diff --git a/profiles/manifest.go b/profiles/manifest.go
index 0bad100..8528d34 100644
--- a/profiles/manifest.go
+++ b/profiles/manifest.go
@@ -13,6 +13,7 @@
 	"time"
 
 	"v.io/jiri/jiri"
+	"v.io/jiri/runutil"
 )
 
 const (
@@ -242,9 +243,9 @@
 	defer pdb.Unlock()
 	pdb.db = make(map[string]*Profile)
 
-	data, err := jirix.Run().ReadFile(filename)
+	data, err := jirix.NewSeq().ReadFile(filename)
 	if err != nil {
-		if os.IsNotExist(err) {
+		if runutil.IsNotExist(err) {
 			return nil
 		}
 		return err
@@ -317,17 +318,18 @@
 	oldName := filename + ".prev"
 	newName := filename + fmt.Sprintf(".%d", time.Now().UnixNano())
 
-	if err := jirix.Run().WriteFile(newName, data, defaultFileMode); err != nil {
+	s := jirix.NewSeq()
+	if err := s.WriteFile(newName, data, defaultFileMode).Done(); err != nil {
 		return err
 	}
 
-	if jirix.Run().FileExists(filename) {
-		if err := jirix.Run().Rename(filename, oldName); err != nil {
+	if exists, _ := s.FileExists(filename); exists {
+		if err := s.Rename(filename, oldName).Done(); err != nil {
 			return err
 		}
 	}
 
-	if err := jirix.Run().Rename(newName, filename); err != nil {
+	if err := s.Rename(newName, filename).Done(); err != nil {
 		return err
 	}
 
diff --git a/project.go b/project.go
index f59871a..fa2e77b 100644
--- a/project.go
+++ b/project.go
@@ -7,7 +7,6 @@
 import (
 	"encoding/json"
 	"fmt"
-	"path"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -62,13 +61,14 @@
 	if err != nil {
 		return err
 	}
-	projects := map[string]project.Project{}
+	var projects project.Projects
 	if len(args) > 0 {
 		for _, arg := range args {
-			if p, ok := localProjects[arg]; ok {
-				projects[p.Name] = p
+			p, err := localProjects.FindUnique(arg)
+			if err != nil {
+				fmt.Fprintf(jirix.Stderr(), "Error finding local project %q: %v.\n", p.Name, err)
 			} else {
-				fmt.Fprintf(jirix.Stderr(), "Local project %q not found.\n", p.Name)
+				projects[p.Key()] = p
 			}
 		}
 	} else {
@@ -94,21 +94,21 @@
 	if err != nil {
 		return err
 	}
-	names := []string{}
-	for name := range states {
-		names = append(names, name)
+	var keys project.ProjectKeys
+	for key := range states {
+		keys = append(keys, key)
 	}
-	sort.Strings(names)
+	sort.Sort(keys)
 
-	for _, name := range names {
-		state := states[name]
+	for _, key := range keys {
+		state := states[key]
 		if noPristineFlag {
 			pristine := len(state.Branches) == 1 && state.CurrentBranch == "master" && !state.HasUncommitted && !state.HasUntracked
 			if pristine {
 				continue
 			}
 		}
-		fmt.Fprintf(jirix.Stdout(), "project=%q path=%q\n", path.Base(name), state.Project.Path)
+		fmt.Fprintf(jirix.Stdout(), "project-key=%q path=%q\n", key, state.Project.Path)
 		if branchesFlag {
 			for _, branch := range state.Branches {
 				s := "  "
@@ -144,20 +144,20 @@
 	if err != nil {
 		return err
 	}
-	names := []string{}
-	for name := range states {
-		names = append(names, name)
+	var keys project.ProjectKeys
+	for key := range states {
+		keys = append(keys, key)
 	}
-	sort.Strings(names)
+	sort.Sort(keys)
 
-	// Get the name of the current project.
-	currentProjectName, err := project.CurrentProjectName(jirix)
+	// Get the key of the current project.
+	currentProjectKey, err := project.CurrentProjectKey(jirix)
 	if err != nil {
 		return err
 	}
 	var statuses []string
-	for _, name := range names {
-		state := states[name]
+	for _, key := range keys {
+		state := states[key]
 		status := ""
 		if checkDirtyFlag {
 			if state.HasUncommitted {
@@ -168,8 +168,8 @@
 			}
 		}
 		short := state.CurrentBranch + status
-		long := filepath.Base(name) + ":" + short
-		if name == currentProjectName {
+		long := filepath.Base(states[key].Project.Name) + ":" + short
+		if key == currentProjectKey {
 			if showNameFlag {
 				statuses = append([]string{long}, statuses...)
 			} else {
diff --git a/project/.api b/project/.api
index 7558eaa..82b6d30 100644
--- a/project/.api
+++ b/project/.api
@@ -5,18 +5,24 @@
 pkg project, func CleanupProjects(*jiri.X, Projects, bool) error
 pkg project, func CreateSnapshot(*jiri.X, string) error
 pkg project, func CurrentManifest(*jiri.X) (*Manifest, error)
-pkg project, func CurrentProjectName(*jiri.X) (string, error)
+pkg project, func CurrentProjectKey(*jiri.X) (ProjectKey, error)
 pkg project, func DataDirPath(*jiri.X, string) (string, error)
 pkg project, func GerritHost(*jiri.X) (string, error)
-pkg project, func GetProjectStates(*jiri.X, bool) (map[string]*ProjectState, error)
+pkg project, func GetProjectStates(*jiri.X, bool) (map[ProjectKey]*ProjectState, error)
 pkg project, func GitHost(*jiri.X) (string, error)
 pkg project, func InstallTools(*jiri.X, string) error
 pkg project, func LocalProjects(*jiri.X, ScanMode) (Projects, error)
-pkg project, func ParseNames(*jiri.X, []string, map[string]struct{}) (map[string]Project, error)
+pkg project, func ParseNames(*jiri.X, []string, map[string]struct{}) (Projects, error)
 pkg project, func PollProjects(*jiri.X, map[string]struct{}) (Update, error)
 pkg project, func ReadManifest(*jiri.X) (Projects, Tools, error)
 pkg project, func TransitionBinDir(*jiri.X) error
 pkg project, func UpdateUniverse(*jiri.X, bool) error
+pkg project, method (Project) Key() ProjectKey
+pkg project, method (ProjectKeys) Len() int
+pkg project, method (ProjectKeys) Less(int, int) bool
+pkg project, method (ProjectKeys) Swap(int, int)
+pkg project, method (Projects) Find(string) Projects
+pkg project, method (Projects) FindUnique(string) (Project, error)
 pkg project, method (UnsupportedProtocolErr) Error() string
 pkg project, type BranchState struct
 pkg project, type BranchState struct, HasGerritMessage bool
@@ -62,13 +68,15 @@
 pkg project, type Project struct, Remote string
 pkg project, type Project struct, RemoteBranch string
 pkg project, type Project struct, Revision string
+pkg project, type ProjectKey string
+pkg project, type ProjectKeys []ProjectKey
 pkg project, type ProjectState struct
 pkg project, type ProjectState struct, Branches []BranchState
 pkg project, type ProjectState struct, CurrentBranch string
 pkg project, type ProjectState struct, HasUncommitted bool
 pkg project, type ProjectState struct, HasUntracked bool
 pkg project, type ProjectState struct, Project Project
-pkg project, type Projects map[string]Project
+pkg project, type Projects map[ProjectKey]Project
 pkg project, type ScanMode bool
 pkg project, type Tool struct
 pkg project, type Tool struct, Data string
diff --git a/project/paths.go b/project/paths.go
index 51ed88b..ce0e523 100644
--- a/project/paths.go
+++ b/project/paths.go
@@ -31,10 +31,12 @@
 	if !ok {
 		return "", fmt.Errorf("tool %q not found in the manifest", toolName)
 	}
-	projectName := tool.Project
-	project, ok := projects[projectName]
-	if !ok {
-		return "", fmt.Errorf("project %q not found in the manifest", projectName)
+	// TODO(nlacasse): Tools refer to their project by name, but project name
+	// might not be unique.  We really should stop telling telling tools what their
+	// projects are.
+	project, err := projects.FindUnique(tool.Project)
+	if err != nil {
+		return "", err
 	}
 	return filepath.Join(project.Path, tool.Data), nil
 }
@@ -61,8 +63,6 @@
 	return getHost(jirix, "git")
 }
 
-// toAbs returns the given path rooted in JIRI_ROOT, if it is not already an
-// absolute path.
 func toAbs(jirix *jiri.X, path string) string {
 	if filepath.IsAbs(path) {
 		return path
diff --git a/project/project.go b/project/project.go
index 2364c52..9a59667 100644
--- a/project/project.go
+++ b/project/project.go
@@ -106,8 +106,15 @@
 	Name string `xml:"name,attr"`
 }
 
-// Projects maps project names to their detailed description.
-type Projects map[string]Project
+// ProjectKey is a unique string for a project.
+type ProjectKey string
+
+// ProjectKeys is a slice of ProjectKeys implementing the Sort interface.
+type ProjectKeys []ProjectKey
+
+func (pks ProjectKeys) Len() int           { return len(pks) }
+func (pks ProjectKeys) Less(i, j int) bool { return string(pks[i]) < string(pks[j]) }
+func (pks ProjectKeys) Swap(i, j int)      { pks[i], pks[j] = pks[j], pks[i] }
 
 // Project represents a jiri project.
 type Project struct {
@@ -136,6 +143,47 @@
 	Revision string `xml:"revision,attr"`
 }
 
+// projectKeySeparator is a reserved string used in ProjectKeys.  It cannot
+// occur in Project names or remotes.
+const projectKeySeparator = "="
+
+// Key returns a unique ProjectKey for the project.
+func (p Project) Key() ProjectKey {
+	return ProjectKey(p.Name + projectKeySeparator + p.Remote)
+}
+
+// Projects maps ProjectKeys to Projects.
+type Projects map[ProjectKey]Project
+
+// Find returns all projects in Projects with the given key or name.
+func (ps Projects) Find(name string) Projects {
+	projects := Projects{}
+	for _, p := range ps {
+		if name == p.Name {
+			projects[p.Key()] = p
+		}
+	}
+	return projects
+}
+
+// FindUnique returns the project in Projects with the given key or
+// name, and returns an error if none or multiple matching projects are found.
+func (ps Projects) FindUnique(name string) (Project, error) {
+	var p Project
+	projects := ps.Find(name)
+	if len(projects) == 0 {
+		return p, fmt.Errorf("no projects found with name %q", name)
+	}
+	if len(projects) > 1 {
+		return p, fmt.Errorf("multiple projects found with name %q", name)
+	}
+	// Return the only project in projects.
+	for _, project := range projects {
+		p = project
+	}
+	return p, nil
+}
+
 // Tools maps jiri tool names, to their detailed description.
 type Tools map[string]Tool
 
@@ -218,19 +266,13 @@
 		manifest.Hooks = append(manifest.Hooks, hook)
 	}
 
-	perm := os.FileMode(0755)
-	if err := jirix.Run().MkdirAll(filepath.Dir(path), perm); err != nil {
-		return err
-	}
+	s := jirix.NewSeq()
 	data, err := xml.MarshalIndent(manifest, "", "  ")
 	if err != nil {
 		return fmt.Errorf("MarshalIndent(%v) failed: %v", manifest, err)
 	}
-	perm = os.FileMode(0644)
-	if err := jirix.Run().WriteFile(path, data, perm); err != nil {
-		return err
-	}
-	return nil
+	return s.MkdirAll(filepath.Dir(path), os.FileMode(0755)).
+		WriteFile(path, data, os.FileMode(0644)).Done()
 }
 
 const currentManifestFileName = ".current_manifest"
@@ -239,9 +281,9 @@
 // the most recent "jiri update" invocation.
 func CurrentManifest(jirix *jiri.X) (*Manifest, error) {
 	currentManifestPath := toAbs(jirix, currentManifestFileName)
-	bytes, err := jirix.Run().ReadFile(currentManifestPath)
+	bytes, err := jirix.NewSeq().ReadFile(currentManifestPath)
 	if err != nil {
-		if os.IsNotExist(err) {
+		if runutil.IsNotExist(err) {
 			fmt.Fprintf(jirix.Stderr(), `WARNING: Could not find %s.
 The contents of this file are stored as metadata in binaries the jiri
 tool builds. To fix this problem, please run "jiri update".
@@ -265,24 +307,22 @@
 	if err != nil {
 		return fmt.Errorf("MarshalIndent(%v) failed: %v", manifest, err)
 	}
-	if err := jirix.Run().WriteFile(currentManifestPath, bytes, os.FileMode(0644)); err != nil {
-		return err
-	}
-	return nil
+	return jirix.NewSeq().WriteFile(currentManifestPath, bytes, os.FileMode(0644)).Done()
 }
 
-// CurrentProjectName gets the name of the current project from the
-// current directory by reading the jiri project metadata located in a
-// directory at the root of the current repository.
-func CurrentProjectName(jirix *jiri.X) (string, error) {
+// CurrentProjectKey gets the key of the current project from the current
+// directory by reading the jiri project metadata located in a directory at the
+// root of the current repository.
+func CurrentProjectKey(jirix *jiri.X) (ProjectKey, error) {
 	topLevel, err := jirix.Git().TopLevel()
 	if err != nil {
 		return "", nil
 	}
+	s := jirix.NewSeq()
 	metadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir)
-	if _, err := jirix.Run().Stat(metadataDir); err == nil {
+	if _, err := s.Stat(metadataDir); err == nil {
 		metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile)
-		bytes, err := jirix.Run().ReadFile(metadataFile)
+		bytes, err := s.ReadFile(metadataFile)
 		if err != nil {
 			return "", err
 		}
@@ -290,7 +330,7 @@
 		if err := xml.Unmarshal(bytes, &project); err != nil {
 			return "", fmt.Errorf("Unmarshal() failed: %v", err)
 		}
-		return project.Name, nil
+		return project.Key(), nil
 	}
 	return "", nil
 }
@@ -302,12 +342,13 @@
 	if err != nil {
 		return nil, err
 	}
-	defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
+	defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
 
+	s := jirix.NewSeq()
 	for name, project := range projects {
 		switch project.Protocol {
 		case "git":
-			if err := jirix.Run().Chdir(project.Path); err != nil {
+			if err := s.Chdir(project.Path).Done(); err != nil {
 				return nil, err
 			}
 			revision, err := jirix.Git().CurrentRevisionOfBranch("master")
@@ -393,7 +434,7 @@
 	if err != nil {
 		return nil, err
 	}
-	defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
+	defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
 
 	// Gather local & remote project data.
 	localProjects, err := LocalProjects(jirix, FastScan)
@@ -412,6 +453,7 @@
 		return nil, err
 	}
 
+	s := jirix.NewSeq()
 	for _, op := range ops {
 		name := op.Project().Name
 
@@ -429,7 +471,7 @@
 			case "git":
 
 				// Enter project directory - this assumes absolute paths.
-				if err := jirix.Run().Chdir(updateOp.destination); err != nil {
+				if err := s.Chdir(updateOp.destination).Done(); err != nil {
 					return nil, err
 				}
 
@@ -530,16 +572,17 @@
 	if err != nil {
 		return err
 	}
+	s := jirix.NewSeq()
 	// 1. Update all local projects to match their remote counterparts.
 	if err := updateProjects(jirix, remoteProjects, gc); err != nil {
 		return err
 	}
 	// 2. Build all tools in a temporary directory.
-	tmpDir, err := jirix.Run().TempDir("", "tmp-jiri-tools-build")
+	tmpDir, err := s.TempDir("", "tmp-jiri-tools-build")
 	if err != nil {
 		return fmt.Errorf("TempDir() failed: %v", err)
 	}
-	defer collect.Error(func() error { return jirix.Run().RemoveAll(tmpDir) }, &e)
+	defer collect.Error(func() error { return s.RemoveAll(tmpDir).Done() }, &e)
 	if err := buildToolsFromMaster(jirix, remoteTools, tmpDir); err != nil {
 		return err
 	}
@@ -558,13 +601,15 @@
 	if err != nil {
 		return err
 	}
-	defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
+	defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
+
+	s := jirix.NewSeq()
 
 	// Loop through all projects, checking out master and stashing any unstaged
 	// changes.
 	for _, project := range projects {
 		p := project
-		if err := jirix.Run().Chdir(p.Path); err != nil {
+		if err := s.Chdir(p.Path).Done(); err != nil {
 			return err
 		}
 		switch p.Protocol {
@@ -583,7 +628,7 @@
 			// After running the function, return to this project's directory,
 			// checkout the original branch, and stash pop if necessary.
 			defer collect.Error(func() error {
-				if err := jirix.Run().Chdir(p.Path); err != nil {
+				if err := s.Chdir(p.Path).Done(); err != nil {
 					return err
 				}
 				if err := jirix.Git().CheckoutBranch(branch); err != nil {
@@ -618,9 +663,9 @@
 	workspaceSet := map[string]bool{}
 	for _, tool := range tools {
 		toolPkgs = append(toolPkgs, tool.Package)
-		toolProject, ok := projects[tool.Project]
-		if !ok {
-			return fmt.Errorf("project not found for tool %v", tool.Name)
+		toolProject, err := projects.FindUnique(tool.Project)
+		if err != nil {
+			return err
 		}
 		// Identify the Go workspace the tool is in. To this end we use a
 		// heuristic that identifies the maximal suffix of the project path
@@ -646,21 +691,20 @@
 	if envGoPath := os.Getenv("GOPATH"); envGoPath != "" {
 		workspaces = append(workspaces, strings.Split(envGoPath, string(filepath.ListSeparator))...)
 	}
+	s := jirix.NewSeq()
 	var stderr bytes.Buffer
-	opts := jirix.Run().Opts()
+
 	// We unset GOARCH and GOOS because jiri update should always build for the
 	// native architecture and OS.  Also, as of go1.5, setting GOBIN is not
 	// compatible with GOARCH or GOOS.
-	opts.Env = map[string]string{
+	env := map[string]string{
 		"GOARCH": "",
 		"GOOS":   "",
 		"GOBIN":  outputDir,
 		"GOPATH": strings.Join(workspaces, string(filepath.ListSeparator)),
 	}
-	opts.Stdout = ioutil.Discard
-	opts.Stderr = &stderr
 	args := append([]string{"install"}, toolPkgs...)
-	if err := jirix.Run().CommandWithOpts(opts, "go", args...); err != nil {
+	if err := s.Env(env).Capture(ioutil.Discard, &stderr).Last("go", args...); err != nil {
 		return fmt.Errorf("tool build failed\n%v", stderr.String())
 	}
 	return nil
@@ -687,11 +731,11 @@
 		if tool.Package == "" {
 			continue
 		}
-		project, ok := localProjects[tool.Project]
-		if !ok {
-			fmt.Errorf("unknown project %v for tool %v", tool.Project, tool.Name)
+		project, err := localProjects.FindUnique(tool.Project)
+		if err != nil {
+			return err
 		}
-		toolProjects[tool.Project] = project
+		toolProjects[project.Key()] = project
 		toolsToBuild[tool.Name] = tool
 		toolNames = append(toolNames, tool.Name)
 	}
@@ -704,8 +748,7 @@
 
 	// Always log the output of updateFn, irrespective of
 	// the value of the verbose flag.
-	opts := runutil.Opts{Verbose: true}
-	if err := jirix.Run().FunctionWithOpts(opts, updateFn, "build tools: %v", strings.Join(toolNames, " ")); err != nil {
+	if err := jirix.NewSeq().Verbose(true).Call(updateFn, "build tools: %v", strings.Join(toolNames, " ")).Done(); err != nil {
 		fmt.Fprintf(jirix.Stderr(), "%v\n", err)
 		failed = true
 	}
@@ -723,10 +766,11 @@
 	if err != nil {
 		return fmt.Errorf("Getwd() failed: %v", err)
 	}
-	defer collect.Error(func() error { return jirix.Run().Chdir(wd) }, &e)
+	defer collect.Error(func() error { return jirix.NewSeq().Chdir(wd).Done() }, &e)
+	s := jirix.NewSeq()
 	for _, project := range projects {
 		localProjectDir := project.Path
-		if err := jirix.Run().Chdir(localProjectDir); err != nil {
+		if err := s.Chdir(localProjectDir).Done(); err != nil {
 			return err
 		}
 		if err := resetLocalProject(jirix, cleanupBranches, project.RemoteBranch); err != nil {
@@ -786,8 +830,8 @@
 	// Existence of a metadata directory is how we know we've found a
 	// Jiri-maintained project.
 	metadataDir := filepath.Join(absPath, jiri.ProjectMetaDir)
-	if _, err := jirix.Run().Stat(metadataDir); err != nil {
-		if os.IsNotExist(err) {
+	if _, err := jirix.NewSeq().Stat(metadataDir); err != nil {
+		if runutil.IsNotExist(err) {
 			return false, nil
 		}
 		return false, err
@@ -801,7 +845,7 @@
 	var project Project
 	absPath := toAbs(jirix, path)
 	metadataFile := filepath.Join(absPath, jiri.ProjectMetaDir, jiri.ProjectMetaFile)
-	bytes, err := jirix.Run().ReadFile(metadataFile)
+	bytes, err := jirix.NewSeq().ReadFile(metadataFile)
 	if err != nil {
 		return project, err
 	}
@@ -828,14 +872,14 @@
 		if absPath != project.Path {
 			return fmt.Errorf("project %v has path %v but was found in %v", project.Name, project.Path, absPath)
 		}
-		if p, ok := projects[project.Name]; ok {
-			return fmt.Errorf("name conflict: both %v and %v contain the project %v", p.Path, project.Path, project.Name)
+		if p, ok := projects[project.Key()]; ok {
+			return fmt.Errorf("name conflict: both %v and %v contain project with key %v", p.Path, project.Path, project.Key())
 		}
-		projects[project.Name] = project
+		projects[project.Key()] = project
 	}
 
 	// Recurse into all the sub directories.
-	fileInfos, err := jirix.Run().ReadDir(path)
+	fileInfos, err := jirix.NewSeq().ReadDir(path)
 	if err != nil {
 		return err
 	}
@@ -867,14 +911,14 @@
 		return fmt.Errorf("MkdirAll(%v) failed: %v", binDir, err)
 	}
 	failed := false
+	s := jirix.NewSeq()
 	for _, fi := range fis {
 		installFn := func() error {
 			src := filepath.Join(dir, fi.Name())
 			dst := filepath.Join(binDir, fi.Name())
-			return jirix.Run().Rename(src, dst)
+			return jirix.NewSeq().Rename(src, dst).Done()
 		}
-		opts := runutil.Opts{Verbose: true}
-		if err := jirix.Run().FunctionWithOpts(opts, installFn, "install tool %q", fi.Name()); err != nil {
+		if err := s.Verbose(true).Call(installFn, "install tool %q", fi.Name()).Done(); err != nil {
 			fmt.Fprintf(jirix.Stderr(), "%v\n", err)
 			failed = true
 		}
@@ -897,14 +941,15 @@
 //
 // TODO(toddw): Remove this logic after the transition to .jiri_root is done.
 func TransitionBinDir(jirix *jiri.X) error {
+	s := jirix.NewSeq()
 	oldDir, newDir := filepath.Join(jirix.Root, "devtools", "bin"), jirix.BinDir()
-	switch info, err := jirix.Run().Lstat(oldDir); {
-	case os.IsNotExist(err):
+	switch info, err := s.Lstat(oldDir); {
+	case runutil.IsNotExist(err):
 		// Drop down to create the symlink below.
 	case err != nil:
 		return fmt.Errorf("Failed to stat old bin dir: %v", err)
 	case info.Mode()&os.ModeSymlink != 0:
-		link, err := jirix.NewSeq().Readlink(oldDir)
+		link, err := s.Readlink(oldDir)
 		if err != nil {
 			return fmt.Errorf("Failed to read link from old bin dir: %v", err)
 		}
@@ -917,9 +962,9 @@
 		// The old dir exists, and either it's not a symlink, or it's a symlink that
 		// doesn't point to the new dir.  Move the old dir to the backup location.
 		backupDir := newDir + ".BACKUP"
-		switch _, err := jirix.Run().Stat(backupDir); {
-		case os.IsNotExist(err):
-			if err := jirix.NewSeq().Rename(oldDir, backupDir).Done(); err != nil {
+		switch _, err := s.Stat(backupDir); {
+		case runutil.IsNotExist(err):
+			if err := s.Rename(oldDir, backupDir).Done(); err != nil {
 				return fmt.Errorf("Failed to backup old bin dir %v to %v: %v", oldDir, backupDir, err)
 			}
 			// Drop down to create the symlink below.
@@ -930,7 +975,7 @@
 		}
 	}
 	// Create the symlink.
-	if err := jirix.NewSeq().MkdirAll(filepath.Dir(oldDir), 0755).Symlink(newDir, oldDir).Done(); err != nil {
+	if err := s.MkdirAll(filepath.Dir(oldDir), 0755).Symlink(newDir, oldDir).Done(); err != nil {
 		return fmt.Errorf("Failed to symlink to new bin dir %v from %v: %v", newDir, oldDir, err)
 	}
 	return nil
@@ -940,6 +985,7 @@
 func runHooks(jirix *jiri.X, hooks Hooks) error {
 	jirix.TimerPush("run hooks")
 	defer jirix.TimerPop()
+	s := jirix.NewSeq()
 	for _, hook := range hooks {
 		command := hook.Path
 		args := []string{}
@@ -950,7 +996,7 @@
 		for _, arg := range hook.Args {
 			args = append(args, arg.Arg)
 		}
-		if err := jirix.Run().Command(command, args...); err != nil {
+		if err := s.Last(command, args...); err != nil {
 			return fmt.Errorf("Hook %v failed: %v command: %v args: %v", hook.Name, err, command, args)
 		}
 	}
@@ -990,13 +1036,13 @@
 			return UnsupportedProtocolErr(project.Protocol)
 		}
 	}
-	return ApplyToLocalMaster(jirix, Projects{project.Name: project}, fn)
+	return ApplyToLocalMaster(jirix, Projects{project.Key(): project}, fn)
 }
 
 // loadManifest loads the given manifest, processing all of its
 // imports, projects and tools settings.
 func loadManifest(jirix *jiri.X, path string, hosts Hosts, projects Projects, tools Tools, hooks Hooks, stack map[string]struct{}) error {
-	data, err := jirix.Run().ReadFile(path)
+	data, err := jirix.NewSeq().ReadFile(path)
 	if err != nil {
 		return err
 	}
@@ -1021,10 +1067,13 @@
 	}
 	// Process all projects.
 	for _, project := range m.Projects {
+		if strings.Contains(project.Name, projectKeySeparator) {
+			return fmt.Errorf("project name cannot contain %q: %q", projectKeySeparator, project.Name)
+		}
 		if project.Exclude {
 			// Exclude the project in case it was
 			// previously included.
-			delete(projects, project.Name)
+			delete(projects, project.Key())
 			continue
 		}
 		// Replace the relative path with an absolute one.
@@ -1047,7 +1096,7 @@
 		if project.RemoteBranch == "" {
 			project.RemoteBranch = "master"
 		}
-		projects[project.Name] = project
+		projects[project.Key()] = project
 	}
 	// Process all tools.
 	for _, tool := range m.Tools {
@@ -1075,10 +1124,9 @@
 			delete(hooks, hook.Name)
 			continue
 		}
-		project, found := projects[hook.Project]
-		if !found {
-			return fmt.Errorf("hook %v specified project %v which was not found",
-				hook.Name, hook.Project)
+		project, err := projects.FindUnique(hook.Project)
+		if err != nil {
+			return fmt.Errorf("error while finding project %q for hook %q: %v", hook.Project, hook.Name, err)
 		}
 		// Replace project-relative path with absolute path.
 		hook.Path = filepath.Join(project.Path, hook.Path)
@@ -1106,8 +1154,9 @@
 	if err != nil {
 		return err
 	}
-	defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
-	if err := jirix.Run().Chdir(project.Path); err != nil {
+	defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
+	s := jirix.NewSeq()
+	if err := s.Chdir(project.Path).Done(); err != nil {
 		return err
 	}
 	switch project.Protocol {
@@ -1119,8 +1168,7 @@
 		if current != "master" {
 			line1 := fmt.Sprintf(`NOTE: "jiri update" only updates the "master" branch and the current branch is %q`, current)
 			line2 := fmt.Sprintf(`to update the %q branch once the master branch is updated, run "git merge master"`, current)
-			opts := runutil.Opts{Verbose: true}
-			jirix.Run().OutputWithOpts(opts, []string{line1, line2})
+			s.Verbose(true).Output([]string{line1, line2})
 		}
 		return nil
 	default:
@@ -1194,12 +1242,12 @@
 	}
 	failed := false
 	manifest := &Manifest{Label: jirix.Manifest()}
+	s := jirix.NewSeq()
 	for _, op := range ops {
 		updateFn := func() error { return op.Run(jirix, manifest) }
 		// Always log the output of updateFn, irrespective of
 		// the value of the verbose flag.
-		opts := runutil.Opts{Verbose: true}
-		if err := jirix.Run().FunctionWithOpts(opts, updateFn, "%v", op); err != nil {
+		if err := s.Verbose(true).Call(updateFn, "%v", op).Done(); err != nil {
 			fmt.Fprintf(jirix.Stderr(), "%v\n", err)
 			failed = true
 		}
@@ -1221,13 +1269,14 @@
 	if err != nil {
 		return err
 	}
-	defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
-	if err := jirix.Run().MkdirAll(metadataDir, os.FileMode(0755)); err != nil {
+	defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
+
+	s := jirix.NewSeq()
+	if err := s.MkdirAll(metadataDir, os.FileMode(0755)).
+		Chdir(metadataDir).Done(); err != nil {
 		return err
 	}
-	if err := jirix.Run().Chdir(metadataDir); err != nil {
-		return err
-	}
+
 	// Replace absolute project paths with relative paths to make it
 	// possible to move the $JIRI_ROOT directory locally.
 	relPath, err := toRel(jirix, project.Path)
@@ -1241,10 +1290,8 @@
 	}
 	metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile)
 	tmpMetadataFile := metadataFile + ".tmp"
-	if err := jirix.Run().WriteFile(tmpMetadataFile, bytes, os.FileMode(0644)); err != nil {
-		return err
-	}
-	if err := jirix.Run().Rename(tmpMetadataFile, metadataFile); err != nil {
+	if err := s.WriteFile(tmpMetadataFile, bytes, os.FileMode(0644)).
+		Rename(tmpMetadataFile, metadataFile).Done(); err != nil {
 		return err
 	}
 	return nil
@@ -1342,20 +1389,19 @@
 	if err != nil {
 		return err
 	}
+	s := jirix.NewSeq()
 
 	path, perm := filepath.Dir(op.destination), os.FileMode(0755)
-	if err := jirix.Run().MkdirAll(path, perm); err != nil {
-		return err
-	}
+	tmpDirPrefix := strings.Replace(op.Project().Name, "/", ".", -1) + "-"
+
 	// Create a temporary directory for the initial setup of the
 	// project to prevent an untimely termination from leaving the
 	// $JIRI_ROOT directory in an inconsistent state.
-	tmpDirPrefix := strings.Replace(op.Project().Name, "/", ".", -1) + "-"
-	tmpDir, err := jirix.Run().TempDir(path, tmpDirPrefix)
+	tmpDir, err := s.MkdirAll(path, perm).TempDir(path, tmpDirPrefix)
 	if err != nil {
 		return err
 	}
-	defer collect.Error(func() error { return jirix.Run().RemoveAll(tmpDir) }, &e)
+	defer collect.Error(func() error { return jirix.NewSeq().RemoveAll(tmpDir).Done() }, &e)
 	switch op.project.Protocol {
 	case "git":
 		if err := jirix.Git().Clone(op.project.Remote, tmpDir); err != nil {
@@ -1371,12 +1417,12 @@
 			gitHookDir := filepath.Join(tmpDir, ".git", "hooks")
 			for _, githook := range host.GitHooks {
 				mdir := jirix.ManifestDir()
-				src, err := jirix.Run().ReadFile(filepath.Join(mdir, githook.Path))
+				src, err := s.ReadFile(filepath.Join(mdir, githook.Path))
 				if err != nil {
 					return err
 				}
 				dst := filepath.Join(gitHookDir, githook.Name)
-				if err := jirix.Run().WriteFile(dst, src, perm); err != nil {
+				if err := s.WriteFile(dst, src, perm).Done(); err != nil {
 					return err
 				}
 			}
@@ -1386,11 +1432,9 @@
 		// write to .git/info/exclude
 		excludeString := "/.jiri/\n"
 		excludeDir := filepath.Join(tmpDir, ".git", "info")
-		if err := jirix.Run().MkdirAll(excludeDir, os.FileMode(0750)); err != nil {
-			return err
-		}
 		excludeFile := filepath.Join(excludeDir, "exclude")
-		if err := jirix.Run().WriteFile(excludeFile, []byte(excludeString), perm); err != nil {
+		if err := s.MkdirAll(excludeDir, os.FileMode(0750)).
+			WriteFile(excludeFile, []byte(excludeString), perm).Done(); err != nil {
 			return err
 		}
 
@@ -1398,8 +1442,8 @@
 		if err != nil {
 			return err
 		}
-		defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
-		if err := jirix.Run().Chdir(tmpDir); err != nil {
+		defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
+		if err := s.Chdir(tmpDir).Done(); err != nil {
 			return err
 		}
 		if err := jirix.Git().Reset(op.project.Revision); err != nil {
@@ -1411,10 +1455,8 @@
 	if err := writeMetadata(jirix, op.project, tmpDir); err != nil {
 		return err
 	}
-	if err := jirix.Run().Chmod(tmpDir, os.FileMode(0755)); err != nil {
-		return err
-	}
-	if err := jirix.Run().Rename(tmpDir, op.destination); err != nil {
+	if err := s.Chmod(tmpDir, os.FileMode(0755)).
+		Rename(tmpDir, op.destination).Done(); err != nil {
 		return err
 	}
 	if err := resetProject(jirix, op.project); err != nil {
@@ -1429,8 +1471,8 @@
 
 func (op createOperation) Test(jirix *jiri.X, updates *fsUpdates) error {
 	// Check the local file system.
-	if _, err := jirix.Run().Stat(op.destination); err != nil {
-		if !os.IsNotExist(err) {
+	if _, err := jirix.NewSeq().Stat(op.destination); err != nil {
+		if !runutil.IsNotExist(err) {
 			return err
 		}
 	} else if !updates.isDeleted(op.destination) {
@@ -1448,6 +1490,7 @@
 }
 
 func (op deleteOperation) Run(jirix *jiri.X, _ *Manifest) error {
+	s := jirix.NewSeq()
 	if op.gc {
 		// Never delete the <JiriProject>.
 		if op.project.Name == JiriProject {
@@ -1456,8 +1499,7 @@
 				"however this project is required for correct operation of the jiri",
 				"development tools and will thus not be deleted",
 			}
-			opts := runutil.Opts{Verbose: true}
-			jirix.Run().OutputWithOpts(opts, lines)
+			s.Verbose(true).Output(lines)
 			return nil
 		}
 		// Never delete projects with non-master branches, uncommitted
@@ -1481,11 +1523,10 @@
 				"however this project either contains non-master branches, uncommitted",
 				"work, or untracked files and will thus not be deleted",
 			}
-			opts := runutil.Opts{Verbose: true}
-			jirix.Run().OutputWithOpts(opts, lines)
+			s.Verbose(true).Output(lines)
 			return nil
 		}
-		return jirix.Run().RemoveAll(op.source)
+		return s.RemoveAll(op.source).Done()
 	}
 	lines := []string{
 		fmt.Sprintf("NOTE: project %v was not found in the project manifest", op.project.Name),
@@ -1493,8 +1534,7 @@
 		fmt.Sprintf(`if you no longer need it, invoke "rm -rf %v"`, op.source),
 		`or invoke "jiri update -gc" to remove all such local projects`,
 	}
-	opts := runutil.Opts{Verbose: true}
-	jirix.Run().OutputWithOpts(opts, lines)
+	s.Verbose(true).Output(lines)
 	return nil
 }
 
@@ -1503,8 +1543,8 @@
 }
 
 func (op deleteOperation) Test(jirix *jiri.X, updates *fsUpdates) error {
-	if _, err := jirix.Run().Stat(op.source); err != nil {
-		if os.IsNotExist(err) {
+	if _, err := jirix.NewSeq().Stat(op.source); err != nil {
+		if runutil.IsNotExist(err) {
 			return fmt.Errorf("cannot delete %q as it does not exist", op.source)
 		}
 		return err
@@ -1519,11 +1559,10 @@
 }
 
 func (op moveOperation) Run(jirix *jiri.X, manifest *Manifest) error {
+	s := jirix.NewSeq()
 	path, perm := filepath.Dir(op.destination), os.FileMode(0755)
-	if err := jirix.Run().MkdirAll(path, perm); err != nil {
-		return err
-	}
-	if err := jirix.Run().Rename(op.source, op.destination); err != nil {
+	if err := s.MkdirAll(path, perm).
+		Rename(op.source, op.destination).Done(); err != nil {
 		return err
 	}
 	if err := reportNonMaster(jirix, op.project); err != nil {
@@ -1543,14 +1582,15 @@
 }
 
 func (op moveOperation) Test(jirix *jiri.X, updates *fsUpdates) error {
-	if _, err := jirix.Run().Stat(op.source); err != nil {
-		if os.IsNotExist(err) {
+	s := jirix.NewSeq()
+	if _, err := s.Stat(op.source); err != nil {
+		if runutil.IsNotExist(err) {
 			return fmt.Errorf("cannot move %q to %q as the source does not exist", op.source, op.destination)
 		}
 		return err
 	}
-	if _, err := jirix.Run().Stat(op.destination); err != nil {
-		if !os.IsNotExist(err) {
+	if _, err := s.Stat(op.destination); err != nil {
+		if !runutil.IsNotExist(err) {
 			return err
 		}
 	} else {
@@ -1658,16 +1698,16 @@
 // projects.
 func computeOperations(localProjects, remoteProjects Projects, gc bool) (operations, error) {
 	result := operations{}
-	allProjects := map[string]struct{}{}
-	for name, _ := range localProjects {
-		allProjects[name] = struct{}{}
+	allProjects := map[ProjectKey]struct{}{}
+	for _, p := range localProjects {
+		allProjects[p.Key()] = struct{}{}
 	}
-	for name, _ := range remoteProjects {
-		allProjects[name] = struct{}{}
+	for _, p := range remoteProjects {
+		allProjects[p.Key()] = struct{}{}
 	}
-	for name, _ := range allProjects {
-		if localProject, ok := localProjects[name]; ok {
-			if remoteProject, ok := remoteProjects[name]; ok {
+	for key, _ := range allProjects {
+		if localProject, ok := localProjects[key]; ok {
+			if remoteProject, ok := remoteProjects[key]; ok {
 				if localProject.Path != remoteProject.Path {
 					// moveOperation also does an update, so we don't need to
 					// check the revision here.
@@ -1698,14 +1738,14 @@
 					source:      localProject.Path,
 				}, gc})
 			}
-		} else if remoteProject, ok := remoteProjects[name]; ok {
+		} else if remoteProject, ok := remoteProjects[key]; ok {
 			result = append(result, createOperation{commonOperation{
 				destination: remoteProject.Path,
 				project:     remoteProject,
 				source:      "",
 			}})
 		} else {
-			return nil, fmt.Errorf("project %v does not exist", name)
+			return nil, fmt.Errorf("project with key %v does not exist", key)
 		}
 	}
 	sort.Sort(result)
@@ -1714,23 +1754,25 @@
 
 // ParseNames identifies the set of projects that a jiri command should
 // be applied to.
-func ParseNames(jirix *jiri.X, args []string, defaultProjects map[string]struct{}) (map[string]Project, error) {
-	projects, _, err := ReadManifest(jirix)
+func ParseNames(jirix *jiri.X, args []string, defaultProjects map[string]struct{}) (Projects, error) {
+	manifestProjects, _, err := ReadManifest(jirix)
 	if err != nil {
 		return nil, err
 	}
-	result := map[string]Project{}
+	result := Projects{}
 	if len(args) == 0 {
 		// Use the default set of projects.
 		args = set.String.ToSlice(defaultProjects)
 	}
 	for _, name := range args {
-		if project, ok := projects[name]; ok {
-			result[name] = project
-		} else {
+		projects := manifestProjects.Find(name)
+		if len(projects) == 0 {
 			// Issue a warning if the target project does not exist in the
 			// project manifest.
-			fmt.Fprintf(jirix.Stderr(), "WARNING: project %q does not exist in the project manifest and will be skipped\n", name)
+			fmt.Fprintf(jirix.Stderr(), "project %q does not exist in the project manifest", name)
+		}
+		for _, project := range projects {
+			result[project.Key()] = project
 		}
 	}
 	return result, nil
diff --git a/project/project_test.go b/project/project_test.go
index 9230999..03eb785 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -19,6 +19,7 @@
 	"v.io/jiri/jiri"
 	"v.io/jiri/jiritest"
 	"v.io/jiri/project"
+	"v.io/jiri/runutil"
 )
 
 func addRemote(t *testing.T, jirix *jiri.X, localProject, name, remoteProject string) {
@@ -26,8 +27,8 @@
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	defer jirix.Run().Chdir(cwd)
-	if err := jirix.Run().Chdir(localProject); err != nil {
+	defer jirix.NewSeq().Chdir(cwd)
+	if err := jirix.NewSeq().Chdir(localProject).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	if err := jirix.Git().AddRemote(name, remoteProject); err != nil {
@@ -36,7 +37,7 @@
 }
 
 func checkReadme(t *testing.T, jirix *jiri.X, project, message string) {
-	if _, err := jirix.Run().Stat(project); err != nil {
+	if _, err := jirix.NewSeq().Stat(project); err != nil {
 		t.Fatalf("%v", err)
 	}
 	readmeFile := filepath.Join(project, "README")
@@ -51,7 +52,7 @@
 
 // Checks that /.jiri/ is ignored in a local project checkout
 func checkGitIgnore(t *testing.T, jirix *jiri.X, project string) {
-	if _, err := jirix.Run().Stat(project); err != nil {
+	if _, err := jirix.NewSeq().Stat(project); err != nil {
 		t.Fatalf("%v", err)
 	}
 	gitInfoExcludeFile := filepath.Join(project, ".git", "info", "exclude")
@@ -106,7 +107,7 @@
 
 func createRemoteManifest(t *testing.T, jirix *jiri.X, dir string, remotes []string) {
 	manifestDir, perm := filepath.Join(dir, "v2"), os.FileMode(0755)
-	if err := jirix.Run().MkdirAll(manifestDir, perm); err != nil {
+	if err := jirix.NewSeq().MkdirAll(manifestDir, perm).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	manifest := project.Manifest{}
@@ -145,8 +146,8 @@
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	defer jirix.Run().Chdir(cwd)
-	if err := jirix.Run().Chdir(manifestDir); err != nil {
+	defer jirix.NewSeq().Chdir(cwd)
+	if err := jirix.NewSeq().Chdir(manifestDir).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	if err := jirix.Git().CommitFile(manifestFile, "creating manifest"); err != nil {
@@ -168,7 +169,7 @@
 	commitManifest(t, jirix, &manifest, manifestDir)
 }
 
-func deleteProject(t *testing.T, jirix *jiri.X, manifestDir, name string) {
+func deleteProject(t *testing.T, jirix *jiri.X, manifestDir, remote string) {
 	manifestFile := filepath.Join(manifestDir, "v2", "default")
 	data, err := ioutil.ReadFile(manifestFile)
 	if err != nil {
@@ -178,7 +179,7 @@
 	if err := xml.Unmarshal(data, &manifest); err != nil {
 		t.Fatalf("Unmarshal() failed: %v\n%v", err, data)
 	}
-	manifest.Projects = append(manifest.Projects, project.Project{Exclude: true, Name: name})
+	manifest.Projects = append(manifest.Projects, project.Project{Exclude: true, Name: remote, Remote: remote})
 	commitManifest(t, jirix, &manifest, manifestDir)
 }
 
@@ -188,8 +189,8 @@
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	defer jirix.Run().Chdir(cwd)
-	if err := jirix.Run().Chdir(name); err != nil {
+	defer jirix.NewSeq().Chdir(cwd)
+	if err := jirix.NewSeq().Chdir(name).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	revision, err := jirix.Git().CurrentRevision()
@@ -265,15 +266,16 @@
 
 func setupNewProject(t *testing.T, jirix *jiri.X, dir, name string, ignore bool) string {
 	projectDir, perm := filepath.Join(dir, name), os.FileMode(0755)
-	if err := jirix.Run().MkdirAll(projectDir, perm); err != nil {
+	s := jirix.NewSeq()
+	if err := s.MkdirAll(projectDir, perm).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	cwd, err := os.Getwd()
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	defer jirix.Run().Chdir(cwd)
-	if err := jirix.Run().Chdir(projectDir); err != nil {
+	defer jirix.NewSeq().Chdir(cwd)
+	if err := s.Chdir(projectDir).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	if err := jirix.Git().Init(projectDir); err != nil {
@@ -281,7 +283,7 @@
 	}
 	if ignore {
 		ignoreFile := filepath.Join(projectDir, ".gitignore")
-		if err := jirix.Run().WriteFile(ignoreFile, []byte(jiri.ProjectMetaDir), os.FileMode(0644)); err != nil {
+		if err := s.WriteFile(ignoreFile, []byte(jiri.ProjectMetaDir), os.FileMode(0644)).Done(); err != nil {
 			t.Fatalf("%v", err)
 		}
 		if err := jirix.Git().Add(ignoreFile); err != nil {
@@ -295,11 +297,12 @@
 }
 
 func writeEmptyMetadata(t *testing.T, jirix *jiri.X, projectDir string) {
-	if err := jirix.Run().Chdir(projectDir); err != nil {
+	s := jirix.NewSeq()
+	if err := s.Chdir(projectDir).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	metadataDir := filepath.Join(projectDir, jiri.ProjectMetaDir)
-	if err := jirix.Run().MkdirAll(metadataDir, os.FileMode(0755)); err != nil {
+	if err := s.MkdirAll(metadataDir, os.FileMode(0755)).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	bytes, err := xml.Marshal(project.Project{})
@@ -307,7 +310,7 @@
 		t.Fatalf("Marshal() failed: %v", err)
 	}
 	metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile)
-	if err := jirix.Run().WriteFile(metadataFile, bytes, os.FileMode(0644)); err != nil {
+	if err := s.WriteFile(metadataFile, bytes, os.FileMode(0644)).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 }
@@ -321,8 +324,8 @@
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	defer jirix.Run().Chdir(cwd)
-	if err := jirix.Run().Chdir(projectDir); err != nil {
+	defer jirix.NewSeq().Chdir(cwd)
+	if err := jirix.NewSeq().Chdir(projectDir).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	if err := jirix.Git().CommitFile(path, "creating README"); err != nil {
@@ -335,8 +338,8 @@
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	defer jirix.Run().Chdir(cwd)
-	if err := jirix.Run().Chdir(projectDir); err != nil {
+	defer jirix.NewSeq().Chdir(cwd)
+	if err := jirix.NewSeq().Chdir(projectDir).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	if err := jirix.Git().CreateAndCheckoutBranch(branch); err != nil {
@@ -349,8 +352,8 @@
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	defer jirix.Run().Chdir(cwd)
-	if err := jirix.Run().Chdir(projectDir); err != nil {
+	defer jirix.NewSeq().Chdir(cwd)
+	if err := jirix.NewSeq().Chdir(projectDir).Done(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	if err := jirix.Git().Reset("origin/master"); err != nil {
@@ -414,7 +417,7 @@
 
 	// Check that deleting a project forces LocalProjects to run a full scan,
 	// even if FastScan is specified.
-	if err := jirix.Run().RemoveAll(projectPaths[0]); err != nil {
+	if err := jirix.NewSeq().RemoveAll(projectPaths[0]).Done(); err != nil {
 		t.Fatalf("RemoveAll(%v) failed: %v", projectPaths[0])
 	}
 	foundProjects, err = project.LocalProjects(jirix, project.FastScan)
@@ -543,10 +546,10 @@
 	checkDeleteFn := func(i int, revision string) {
 		if i == 3 {
 			localProject := filepath.Join(localDir, localProjectName(i))
-			if _, err := jirix.Run().Stat(localProject); err == nil {
+			if _, err := jirix.NewSeq().Stat(localProject); err == nil {
 				t.Fatalf("project %v has not been deleted", localProject)
 			} else {
-				if !os.IsNotExist(err) {
+				if !runutil.IsNotExist(err) {
 					t.Fatalf("%v", err)
 				}
 			}
diff --git a/project/state.go b/project/state.go
index 3b19dd7..49c350d 100644
--- a/project/state.go
+++ b/project/state.go
@@ -5,10 +5,10 @@
 package project
 
 import (
-	"os"
 	"path/filepath"
 
 	"v.io/jiri/jiri"
+	"v.io/jiri/runutil"
 	"v.io/jiri/tool"
 )
 
@@ -39,8 +39,8 @@
 		for _, branch := range branches {
 			file := filepath.Join(state.Project.Path, jiri.ProjectMetaDir, branch, ".gerrit_commit_message")
 			hasFile := true
-			if _, err := jirix.Run().Stat(file); err != nil {
-				if !os.IsNotExist(err) {
+			if _, err := jirix.NewSeq().Stat(file); err != nil {
+				if !runutil.IsNotExist(err) {
 					ch <- err
 					return
 				}
@@ -70,18 +70,18 @@
 	ch <- nil
 }
 
-func GetProjectStates(jirix *jiri.X, checkDirty bool) (map[string]*ProjectState, error) {
+func GetProjectStates(jirix *jiri.X, checkDirty bool) (map[ProjectKey]*ProjectState, error) {
 	projects, err := LocalProjects(jirix, FastScan)
 	if err != nil {
 		return nil, err
 	}
-	states := make(map[string]*ProjectState, len(projects))
+	states := make(map[ProjectKey]*ProjectState, len(projects))
 	sem := make(chan error, len(projects))
-	for name, project := range projects {
+	for key, project := range projects {
 		state := &ProjectState{
 			Project: project,
 		}
-		states[name] = state
+		states[key] = state
 		// jirix is not threadsafe, so we make a clone for each goroutine.
 		go setProjectState(jirix.Clone(tool.ContextOpts{}), state, checkDirty, sem)
 	}
diff --git a/runutil/sequence.go b/runutil/sequence.go
index ec35908..1dc1412 100644
--- a/runutil/sequence.go
+++ b/runutil/sequence.go
@@ -556,7 +556,11 @@
 	if s.err != nil {
 		return s
 	}
-	s.r.OutputWithOpts(s.getOpts(), output)
+	opts := s.getOpts()
+	if s.verbosity != nil {
+		opts.Verbose = *s.verbosity
+	}
+	s.r.OutputWithOpts(opts, output)
 	return s
 }
 
diff --git a/snapshot.go b/snapshot.go
index 16da995..eef6c0c 100644
--- a/snapshot.go
+++ b/snapshot.go
@@ -117,7 +117,7 @@
 		Protocol: "git",
 		Revision: "HEAD",
 	}
-	if err := project.ApplyToLocalMaster(jirix, project.Projects{p.Name: p}, createFn); err != nil {
+	if err := project.ApplyToLocalMaster(jirix, project.Projects{p.Key(): p}, createFn); err != nil {
 		return err
 	}
 	return nil
diff --git a/tool/context.go b/tool/context.go
index fed3577..b885130 100644
--- a/tool/context.go
+++ b/tool/context.go
@@ -140,7 +140,7 @@
 
 // Gerrit returns the Gerrit instance of the context.
 func (ctx Context) Gerrit(host string) *gerrit.Gerrit {
-	return gerrit.New(ctx.run, host)
+	return gerrit.New(ctx.NewSeq(), host)
 }
 
 type gitOpt interface {