---
name: Adopt Project Scope
slug: adopt-project-scope
category: Automation
description: Adopt Project Scope migrates an existing unscoped Task Orchestrator database to project scoping in place. It creates or attaches a project root, re-parents existing work trees, and writes the root UUID to config.yaml when setting up a workspace for multiple projects.
github: "https://github.com/jpicklyk/task-orchestrator/tree/main/claude-plugins/task-orchestrator/skills/adopt-project-scope"
language: Kotlin
stars: 206
forks: 20
install: "npx degit https://github.com/jpicklyk/task-orchestrator/tree/main/claude-plugins/task-orchestrator/skills/adopt-project-scope ~/.claude/skills/adopt-project-scope"
installs_to: ~/.claude/skills/adopt-project-scope
source_path: claude-plugins/task-orchestrator/skills/adopt-project-scope/SKILL.md
collection_size: 25
category_size: 1956
collection_url: "https://dirskills.com/collections/jpicklyk/task-orchestrator"
added: 2026-09-05T05:28:56.507Z
last_synced: 2026-09-05T05:28:56.507Z
canonical_url: "https://dirskills.com/skills/adopt-project-scope"
---

# Adopt Project Scope

Adopt Project Scope migrates an existing unscoped Task Orchestrator database to project scoping in place. It creates or attaches a project root, re-parents existing work trees, and writes the root UUID to config.yaml when setting up a workspace for multiple projects.

**Install:**

```bash
npx degit https://github.com/jpicklyk/task-orchestrator/tree/main/claude-plugins/task-orchestrator/skills/adopt-project-scope ~/.claude/skills/adopt-project-scope
```

## README

# Adopt Project Scope — In-Place Migration to Project Scoping

Take an existing, unscoped database (many depth-0 roots, no project anchor) and migrate it in place to the project-scoping convention: one `type=project` root that owns the workspace's work trees, with its UUID written back to `.taskorchestrator/config.yaml` under a `project:` block. Global containers (retrospectives, observations) stay at depth 0; test artifacts are flagged for cleanup, never moved or deleted.

This is a **destructive-by-execution** skill: the EXECUTE step re-parents real items. It defaults to a dry run and never mutates anything without an explicit confirmation. The only thing it ever deletes is its own throwaway server-probe tree.

**Non-goals:** cross-DB consolidation (merging items from a second database); merging multiple existing project anchors into one; any server-side enforcement of scope. This skill adopts a single workspace's single database.

---

## Step 0 — Parse Arguments

- **Project name** — the first non-flag token(s) in `$ARGUMENTS`. If absent, do not guess; ask for it in Step 3 (the dry-run plan) via `AskUserQuestion` before any mutation.
- **`--dry-run`** — if present, the skill stops after Step 3 (the plan) and performs zero mutations regardless of confirmation. The dry run is ALSO always rendered before execution even without the flag; the flag only forces an early exit.

---

## Step 1 — Preflight (abort gates)

Run these checks in order. Each abort prints a clear, user-facing reason and stops.

### 1a — Already adopted?

Read `.taskorchestrator/config.yaml`. If it contains a `project:` block with a non-empty `rootId:`, the workspace is already scoped:

```
◆ Workspace already adopted — config.yaml already points at project root <rootId>.
  Nothing to do. Use /work-summary to inspect the existing project tree.
```

Stop. (If the file is missing entirely, that is fine — a fresh unscoped DB has no config `project:` block. Continue.)

### 1b — Existing anchor already in the DB?

```
query_items(operation="search", type="project", depth=0)
```

(list mode — `query` omitted, filtering by `type` and `depth`.)

- **No results** → continue to 1c.
- **One or more results** → do NOT silently create a second anchor. Offer, via `AskUserQuestion`:
  1. **Attach to existing** — write the existing anchor's UUID into `config.yaml` (skip anchor creation in EXECUTE; still re-parent MOVE roots under it) and continue.
  2. **Abort** — stop and let the user resolve the ambiguity manually.

  If the user picks "attach", record the existing anchor UUID as `ANCHOR_ID` and note that EXECUTE step (a) is skipped.

### 1c — Server capability probe (the depth-sweep fix)

Bulk re-parenting is exactly the scenario that a pre-fix server corrupts: on a pre-#219/#220 server, re-parenting a subtree does NOT recompute descendant depths (bug `99a3e642`), so this skill would silently leave the moved trees with wrong depths. Verify the fix is deployed with a disposable probe, then delete it.

Build a probe where a grandchild's depth MUST change when a middle node is re-parented deeper:

1. Create a throwaway root and a two-level branch under it in one call:
   ```
   manage_items(operation="create", items=[
     { title: "zzprobe-root",  type: "container", priority: "low" }
   ])
   ```
   Capture its UUID as `P`.
2. Create the branch (sequential parentage, small tree). `manage_items` create has no ref/parentRef
   mechanism (only `create_work_tree` does), so a sibling's UUID does not exist until its own create
   call returns — M and G cannot be created in the same batch since G's `parentId` needs M's real UUID:
   ```
   manage_items(operation="create", items=[
     { title: "zzprobe-A",   parentId: P,  priority: "low" }     → capture as A (depth 1)
   ])
   ```
   ```
   manage_items(operation="create", items=[
     { title: "zzprobe-M",   parentId: P,  priority: "low" }     → capture as M (depth 1, sibling of A)
   ])
   ```
   Capture M's returned UUID, then create G referencing it:
   ```
   manage_items(operation="create", items=[
     { title: "zzprobe-G",   parentId: "<M's real UUID>",  priority: "low" }     → capture as G (depth 2, child of M)
   ])
   ```
3. Re-parent the middle node M **under A** (making it deeper):
   ```
   manage_items(operation="update", items=[{ itemId: M, parentId: A }])
   ```
   Post-fix, M becomes depth 2 and G becomes depth 3.
4. Read the grandchild's depth and capture it:
   ```
   query_items(operation="get", itemId=G)
   ```
5. Delete the probe **immediately, before acting on the result** — this runs on every path, pass or fail:
   ```
   manage_items(operation="delete", itemIds=[P], recursive=true)
   ```
6. Now evaluate the captured depth:
   - `depth == 3` → fix is present. Continue.
   - `depth == 2` (stale) → **abort** (probe already deleted):
     ```
     ⊘ Server is missing the reparent depth-sweep fix (bug 99a3e642).
       Bulk re-parenting on this server would corrupt subtree depths.
       Upgrade the Task Orchestrator server (PR #219/#220 or later) and retry.
     ```

**`--dry-run` skips this probe entirely** — no execution will happen, so the capability check is unnecessary and the dry run stays genuinely mutation-free. The probe runs only on the real-execution path, after the user confirms in Step 3.

### 1d — Config-push capability (tolerant)

`manage_project_config` may be absent on older servers. Do not probe it destructively here — just remember to attempt the push in EXECUTE step (d) and, if the tool is unavailable, skip it with a note rather than failing the whole adoption. The local `config.yaml` write (step c) is the source of truth; the server push is a convenience sync.

---

## Step 2 — Inventory & Classify

Full depth-0 census:

```
query_items(operation="overview", excludeTerminal=false, includeChildren=true, limit=<total>)
```

If the response reports `truncated: true`, re-issue with `limit` set to the reported `total` so no root is missed.

Classify **every depth-0 root** into exactly one bucket:

| Bucket | Rule | Action |
|--------|------|--------|
| **KEEP-GLOBAL** | Title matches a global container — `Session Retrospectives`, `Improvement Proposals`; OR the root itself is tagged/typed `agent-observation` (these are standalone depth-0 items, each its own root — never children of a container). | Stays at depth 0. |
| **MOVE** | Any work container or work tree (a root with work/queue/review children), and any standalone work item. Everything that is real project work. | Re-parented under the new anchor. |
| **CLEANUP-CANDIDATE** | Evident test artifacts: titles containing `probe`, `smoke`, `depth-test`, `zzprobe`; or tags matching `mtest-*`. | **Flagged only** — never moved, never deleted. Recommend `/batch-complete`. |

**Ambiguous roots** (no clear rule match — e.g., an untagged standalone item that could be work or a stray container): do NOT guess. Collect them and present via `AskUserQuestion` (`multiSelect`) asking which should MOVE vs stay KEEP-GLOBAL. Test-artifact-looking items always go to CLEANUP-CANDIDATE without asking.

---

## Step 3 — Dry-Run Plan (always shown; `--dry-run` stops here)

Render the full plan before any mutation. Require explicit confirmation to proceed.

```
◆ Adopt Project Scope — Plan   [project: "<name>"]

| Root | ID | Classification | Action |
|------|----|----------------|--------|
| Auth System | `a1b2c3d4` | MOVE | re-parent under new anchor |
| Payments | `e5f6a7b8` | MOVE | re-parent under new anchor |
| Session Retrospectives | `c9d0e1f2` | KEEP-GLOBAL | stays at depth 0 |
| Improvement Proposals | `13243546` | KEEP-GLOBAL | stays at depth 0 |
| zzprobe-smoke-tree | `5768798a` | CLEANUP-CANDIDATE | flag for /batch-complete (not moved) |

Summary: 2 move · 2 stay global · 1 cleanup candidate
Execution will create a "<name>" project anchor and re-parent 2 tree(s) under it.
```

- If the project name is still unknown, ask for it now via `AskUserQuestion` before showing the final counts.
- **`--dry-run`**: stop here. State plainly: "Dry run — no changes made. Re-run without --dry-run to execute."
- Otherwise, confirm via `AskUserQuestion`:
  ```
  ◆ Proceed with adoption? This re-parents <N> tree(s) under a new project anchor.
    1. Yes — execute
    2. No — abort, make no changes
  ```
  Wait for the answer. Only "Yes" proceeds.

---

## Step 4 — Execute

Perform in this order. Record pre-execution counts first (used by Step 5): the depth-0 root count, the MOVE count, and the KEEP-GLOBAL count from Step 2.

**(a) Create the project anchor** — skip if attaching to an existing anchor (Step 1b):

```
manage_items(operation="create", items=[{
  title: "<project name>",
  type: "project",
  summary: "Project anchor — subtree scope for <project name>",
  priority: "low"
}])
```

Capture the new UUID as `ANCHOR_ID`.

**(b) Batch re-parent the MOVE roots** — one call, `items` array (never a sequential call per root):

```
manage_items(operation="update", items=[
  { itemId: "<move-root-1>", parentId: ANCHOR_ID },
  { itemId: "<move-root-2>", parentId: ANCHOR_ID },
  ...
])
```

This relies on the depth-sweep fix verified in Step 1c — each moved subtree's descendant depths and `root_id` are recomputed server-side.

**(c) Write the `project:` block into `config.yaml`** — read-modify-write, preserving ALL existing content:

- Read the current `.taskorchestrator/config.yaml` text (create the file with just the block if it does not exist).
- Insert this top-level block (surgical insert/append — do NOT regenerate or reformat the rest of the file; leave `work_item_schemas:`, `traits:`, `actor_authentication:`, comments, and formatting byte-for-byte intact):
  ```yaml
  project:
    rootId: "<ANCHOR_ID>"
    name: "<project name>"
  ```
- If a `project:` key somehow already exists (it should not — Step 1a would have aborted), update its `rootId`/`name` in place rather than adding a duplicate key.

**(d) Push the config server-side** (tolerant — skip if the tool is absent):

```
manage_project_config(operation="push", rootId=ANCHOR_ID, configYaml="<full config.yaml text>")
```

- A `warning` field about the root's type is only returned when the anchor's `type` is not `project`; since step (a) sets `type: "project"`, none is expected. Report it if present but treat it as non-fatal.
- If `manage_project_config` is unavailable on this server, skip this step and note: "Config pushed locally only — server does not support manage_project_config; the local config.yaml is authoritative."

---

## Step 5 — Verify & Reconcile

Confirm the migration landed correctly. On ANY mismatch, report loudly and do NOT auto-rollback (say the remedy explicitly).

1. **Anchored child count** — the new anchor's direct children must equal the MOVE count:
   ```
   query_items(operation="overview", anchorId=ANCHOR_ID)
   ```
   The anchor's direct-child count must equal `N move`.
2. **Global census** — remaining depth-0 roots must reconcile:
   ```
   query_items(operation="overview", excludeTerminal=false, limit=<total>)
   ```
   Expected depth-0 roots = KEEP-GLOBAL count + 1 (the anchor) + any CLEANUP-CANDIDATE roots left in place (cleanup candidates were flagged, not moved). Confirm the moved roots are no longer at depth 0.
3. **Depth spot-check** — pick one moved tree that had a grandchild; confirm the grandchild's depth increased by exactly 1 from its pre-move value (the anchor added one level above the old root):
   ```
   query_items(operation="get", itemId="<grandchild-of-a-moved-tree>")
   ```

Render a before/after table:

```
✓ Adoption Complete — "<project name>"  (anchor `<8-char-id>`)

|                     | Before | After |
|---------------------|--------|-------|
| Depth-0 roots       | 5      | 4     |
| Under project anchor| 0      | 2     |
| Global containers   | 2      | 2     |
| Cleanup candidates  | 1      | 1 (flagged, not moved) |

↳ Config: project.rootId written to .taskorchestrator/config.yaml
↳ Server push: applied | skipped (tool absent)
↳ Cleanup candidates flagged for /batch-complete: <titles>
```

**On mismatch** (e.g., anchored child count ≠ MOVE count):

```
⚠ Reconciliation mismatch — expected <X> children under the anchor, found <Y>.
  No rollback was performed. To undo: re-parent the affected roots back to depth 0
  with manage_items(operation="update", items=[{ itemId, parentId: null }]) for each,
  then delete the anchor. Investigate before retrying.
```

Remind the user that schema changes / new config require an MCP reconnect (`/mcp`) to take effect if they rely on per-root schema resolution immediately.

---

## Safety Summary

- **Default dry run.** No mutation without an explicit `AskUserQuestion` "Yes".
- **Never deletes user data.** Cleanup candidates are only *flagged*; the sole deletion is the throwaway server probe in Step 1c.
- **Never regenerates config.yaml.** The `project:` block is surgically inserted; all other content is preserved verbatim.
- **Abort, don't corrupt.** A pre-fix server (missing the depth-sweep) aborts in preflight rather than silently corrupting subtree depths.
- **No auto-rollback.** On verify mismatch, the skill reports the manual remedy rather than guessing at an undo.

---

## Verification (before first real run)

Per the specification, exercise the skill against a COPY of a production-shaped DB:

1. **Dry run on the copy** must produce zero mutations and a correct classification table.
2. **Execute on the copy** and confirm Step 5 count reconciliation passes (depth-0 roots, anchored children, grandchild depth +1).

Only after the copy passes should the skill be run against the real database.
