ADR-146: Unified Task ID Strategy for Multi-Project Environments
Status
PROPOSED - February 2, 2026
Context
Two Incompatible Task ID Systems
CODITECT currently operates two task identification systems that have evolved independently and cannot interoperate:
1. Track Nomenclature (ADR-054): Track.Section.Task[.Subtask] format
- Examples:
A.9.1.3,H.8.1.6,C.14.1.1 - Used in: PILOT track files, governance hooks, session logs, CLAUDE.md
- Enforced by:
task-id-validator.pyPreToolUse hook - Regex pattern:
^[A-Z]{1,2}\.\d+\.\d+(\.\d+)?:\s* - Scope: Single project (PILOT), no project prefix, no tenant awareness
2. Work Item Hierarchy (/project command): E{NNN}-T{NNN} format
- Examples:
E001-T001,E003-T042 - Used in:
scripts/work_items.py, sessions.db task_tracking table - Scope: Per-project task queuing, but no track context (which domain? which agent?)
Problems
| Problem | Impact | Affected Component |
|---|---|---|
PILOT track tasks (A.9.1.3) have no project prefix | Cannot reference tasks across projects | Session logs, /cxq queries |
Work items (E001-T001) have no track context | Cannot determine domain or agent | Agent routing, CEF pack selection |
| No project namespacing | Customer projects collide with PILOT IDs | Multi-tenant cloud sync |
task_id_validator.py has no project awareness | Cannot validate cross-project references | Governance hooks |
| Session logs mix task IDs from multiple projects | Ambiguous history when reviewing past work | /cxq, session-log-sync.py |
| Cloud sync requires globally unique IDs | A.9.1.3 exists in both PILOT and customer projects | cloud_sync_client.py, api.coditect.ai |
| No migration path between E-T and track formats | Two parallel systems persist indefinitely | work_items.py, track files |
Constraints
- Backward compatibility is mandatory. Thousands of existing task references in session logs, track files, and CLAUDE.md use bare
A.9.1.3format. These must remain valid. - Human readability. Task IDs appear in tool call descriptions, session logs, and conversation. UUIDs are rejected.
- CLI friendliness. Task IDs are typed in terminal commands. They must be short and unambiguous.
- Multi-tenant isolation. Customer task IDs must never leak across tenant boundaries in cloud sync.
- Incremental adoption. The migration must be phased so existing workflows are not disrupted.
Decision
We adopt a Unified Task ID Format with optional project prefix, project context resolution, a bridge from E-T format, and tenant-scoped cloud sync identifiers.
1. Unified Task ID Format
Grammar:
unified_task_id = [project_prefix ":"] track_id
project_prefix = ALPHA (ALPHA | DIGIT | "-")* # 1-24 chars, case-insensitive
track_id = track "." section "." task ["." subtask]
track = [A-Z]{1,2} # A-N, O-AA, AB-AK
section = DIGIT+
task = DIGIT+
subtask = DIGIT+
Display format: [project:]track.section.task[.subtask]
Examples:
| Task ID | Meaning |
|---|---|
A.9.1.3 | PILOT task (default project, backward compatible) |
PILOT:A.9.1.3 | Same task, explicit project prefix |
OPS-AC:H.9.2.1 | Org project "artifact-cleanup", track H, section 9, task 2, subtask 1 |
CUST-001:A.3.1.2 | Customer 001 project, track A, section 3, task 1, subtask 2 |
WEBAPP:B.2.4 | Project "WEBAPP", track B, section 2, task 4 |
PILOT:AA.5.2.1 | PILOT project, PCF Business track AA, section 5, task 2, subtask 1 |
Project prefix rules:
- 1 to 24 characters, alphanumeric plus hyphens
- Case-insensitive (stored uppercase, displayed uppercase)
- Must be registered in the project registry before use
- Reserved prefixes:
PILOT,SYSTEM,INTERNAL
2. Project Context Resolution
When a task ID has no project prefix, the system resolves the project context through a deterministic fallback chain:
1. Explicit prefix in task ID → Use that project
2. $CODITECT_PROJECT env var → Use env var value
3. .coditect-project file in cwd → Use file contents
4. Default → "PILOT"
Resolution logic (pseudocode):
def resolve_project(task_id: str) -> tuple[str, str]:
"""Returns (project_id, track_task_id)."""
if ":" in task_id:
project, track_task = task_id.split(":", 1)
return (project.upper(), track_task)
# Fallback chain
project = os.environ.get("CODITECT_PROJECT")
if not project:
project_file = Path.cwd() / ".coditect-project"
if project_file.exists():
project = project_file.read_text().strip()
if not project:
project = "PILOT"
return (project.upper(), task_id)
Environment variable:
# Set in shell profile or per-session
export CODITECT_PROJECT=PILOT # Default for CODITECT framework work
export CODITECT_PROJECT=OPS-AC # For artifact-cleanup project
export CODITECT_PROJECT=CUST-001 # For customer 001 project
Project file (.coditect-project):
PILOT
This file is checked into each project repository root, providing automatic project context when cd-ing into the project directory.
3. Project Registry
A project registry tracks all known projects and their allowed tracks. The authoritative registry is stored in org.db (Tier 2) as defined in ADR-144. A local read-only cache file is auto-generated from org.db for fast CLI lookups.
Authoritative source: org.db tables projects and project_tracks (ADR-144)
Local cache: ~/.coditect-data/project-registry.json (auto-generated, read-only)
Schema:
{
"version": "1.0.0",
"projects": {
"PILOT": {
"name": "CODITECT PILOT",
"description": "Core framework development",
"tracks": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N"],
"tenant_id": null,
"created": "2025-06-01T00:00:00Z",
"status": "active"
},
"OPS-AC": {
"name": "Artifact Cleanup Operations",
"description": "Operational artifact cleanup project",
"tracks": ["C", "H", "J"],
"tenant_id": null,
"created": "2026-02-01T00:00:00Z",
"status": "active"
},
"CUST-001": {
"name": "Customer 001 - Acme Corp",
"description": "Acme Corp deployment project",
"tracks": ["A", "B", "C", "D", "E"],
"tenant_id": "tenant_acme_001",
"created": "2026-03-01T00:00:00Z",
"status": "active"
}
}
}
Validation rules:
- A task ID's track letter must exist in the project's
trackslist - Projects with a
tenant_idare isolated: their tasks are only visible within that tenant - New projects are added via
/project-new <name>command or manual registry edit
4. Work Item Bridge (E-T to Unified Format)
The existing E{NNN}-T{NNN} format is bridged to the unified format through a mapping table.
Mapping approach:
| E-T Component | Unified Equivalent |
|---|---|
E{NNN} (Epic) | Maps to a project_id in the registry |
T{NNN} (Task) | Maps to a sequential task number, recorded with track context |
Bridge table (in sessions.db):
CREATE TABLE IF NOT EXISTS work_item_bridge (
et_id TEXT PRIMARY KEY, -- e.g., "E001-T001"
unified_id TEXT NOT NULL, -- e.g., "PILOT:A.9.1.3"
project_id TEXT NOT NULL, -- e.g., "PILOT"
track TEXT NOT NULL, -- e.g., "A"
created_at TEXT DEFAULT (datetime('now')),
migrated_at TEXT, -- When migrated from E-T to unified
UNIQUE(unified_id)
);
Transition in work_items.py:
- Phase 1:
work_items.pyaccepts bothE001-T001andPILOT:A.9.1.3as task identifiers - Phase 2: New tasks created via
work_items.pydefault to unified format - Phase 3:
E001-T001display format shows unified equivalent in parentheses:E001-T001 (PILOT:A.9.1.3) - Phase 4: E-T format deprecated;
work_items.pyemits deprecation warning when E-T format is used
Example transition:
# Phase 1 - Both formats accepted
python3 scripts/work_items.py start E001-T001 # Legacy (works)
python3 scripts/work_items.py start PILOT:A.9.1.3 # Unified (works)
# Phase 2 - New tasks use unified format
python3 scripts/work_items.py create --project PILOT --track A --section 9 --task 2
# Phase 3 - Display shows both
python3 scripts/work_items.py show E001-T001
# Output: E001-T001 (PILOT:A.9.1.3) - Backend API migration [completed]
# Phase 4 - Deprecation warning
python3 scripts/work_items.py start E001-T001
# WARNING: E-T format is deprecated. Use unified format: PILOT:A.9.1.3
5. Cloud Sync Global Uniqueness
Cloud task IDs incorporate tenant namespacing for guaranteed global uniqueness.
Cloud task ID format: {tenant_id}/{project_id}/{track}.{section}.{task}[.{subtask}]
Examples:
| Local Display | Cloud ID |
|---|---|
A.9.1.3 | az1ai/PILOT/A.9.1.3 |
PILOT:A.9.1.3 | az1ai/PILOT/A.9.1.3 |
OPS-AC:H.9.2.1 | az1ai/OPS-AC/H.9.2.1 |
CUST-001:A.3.1.2 | tenant_acme_001/CUST-001/A.3.1.2 |
Properties:
- Collision impossible: Tenant + project + track path guarantees uniqueness
- Tenant isolation: Customer tasks are prefixed with their
tenant_id, notaz1ai - Local shortening: Users see
CUST-001:A.3.1.2, never the full cloud ID - API routing: Cloud API uses
tenant_idfrom the path for multi-tenant isolation viadjango-multitenant
Cloud sync payload:
{
"cloud_task_id": "az1ai/PILOT/A.9.1.3",
"local_task_id": "PILOT:A.9.1.3",
"project_id": "PILOT",
"tenant_id": "az1ai",
"track": "A",
"section": 9,
"task": 1,
"subtask": 3,
"status": "in_progress",
"updated_at": "2026-02-02T14:30:00Z"
}
6. Governance Hook Updates
task-id-validator.py (PreToolUse)
The existing regex pattern ^[A-Z]{1,2}\.\d+\.\d+(\.\d+)?:\s* is extended to accept an optional project prefix:
Updated pattern:
# Old pattern (ADR-054 only)
TASK_ID_PATTERN = re.compile(r'^[A-Z]{1,2}\.\d+\.\d+(\.\d+)?:\s*')
# New pattern (ADR-146 unified)
TASK_ID_PATTERN = re.compile(
r'^(?:([A-Z][A-Z0-9-]{0,23}):)?' # Optional project prefix (group 1)
r'([A-Z]{1,2})' # Track letter(s) (group 2)
r'\.(\d+)' # Section number (group 3)
r'\.(\d+)' # Task number (group 4)
r'(?:\.(\d+))?' # Optional subtask (group 5)
r':\s*' # Colon separator
)
Enhanced validation flow:
def validate_task_id(description: str) -> tuple[bool, str]:
"""Validate task ID in tool description.
Returns (is_valid, message).
"""
match = TASK_ID_PATTERN.match(description)
if not match:
return (False, "Missing or invalid task ID format")
project_prefix = match.group(1) # May be None
track = match.group(2)
# Resolve project context
if project_prefix:
project = project_prefix.upper()
else:
project = os.environ.get("CODITECT_PROJECT", "PILOT")
# Validate project exists in registry (if registry available)
registry = load_project_registry()
if registry and project not in registry["projects"]:
return (False, f"Unknown project: {project}")
# Validate track is allowed for this project
if registry and track not in registry["projects"][project]["tracks"]:
return (False, f"Track {track} not registered for project {project}")
return (True, f"Valid: {project}:{track}.{match.group(3)}.{match.group(4)}")
Backward compatibility: Bare A.9.1.3: still passes validation. The project prefix is resolved from context, defaulting to PILOT.
task-tracking-enforcer.py (PreToolUse:TodoWrite)
Updated to:
- Accept unified format in TodoWrite content
- Resolve project context for track validation
- Pass project context to downstream
task-plan-sync.py
7. Session Log Format
Session log entries gain project context in the task ID bracket:
Current format:
### 2026-02-02T14:30:00Z - [A.9.1.3] Backend API migration
**Author:** Claude (Opus 4.5)
- Completed database migration for user model
Updated format (with explicit project):
### 2026-02-02T14:30:00Z - [PILOT:A.9.1.3] Backend API migration
**Author:** Claude (Opus 4.5)
- Completed database migration for user model
Rules:
- When
$CODITECT_PROJECTisPILOT(default), thePILOT:prefix is optional in session logs for readability - When working in a non-PILOT project, the project prefix is required
/session-logcommand automatically prepends the resolved project context
8. Regex Quick Reference
| Context | Pattern | Example Match |
|---|---|---|
| Bare track ID (ADR-054 compat) | ^[A-Z]{1,2}\.\d+\.\d+(\.\d+)?$ | A.9.1.3 |
| Unified with colon (tool desc) | ^(?:[A-Z][A-Z0-9-]*:)?[A-Z]{1,2}\.\d+\.\d+(\.\d+)?:\s* | PILOT:A.9.1.3: Do thing |
| Cloud ID | ^[a-z0-9_-]+/[A-Z][A-Z0-9-]*/[A-Z]{1,2}\.\d+\.\d+(\.\d+)?$ | az1ai/PILOT/A.9.1.3 |
| Legacy E-T | ^E\d{3}-T\d{3}$ | E001-T001 |
| Session log bracket | \[(?:[A-Z][A-Z0-9-]*:)?[A-Z]{1,2}\.\d+\.\d+(?:\.\d+)?\] | [PILOT:A.9.1.3] |
Consequences
Positive
- Unified identification. One format covers all contexts: local development, cross-project references, cloud sync, and multi-tenant isolation.
- Full backward compatibility. Every existing bare task ID (
A.9.1.3) remains valid and resolves toPILOT:A.9.1.3by default. - Human readable.
CUST-001:A.3.1.2is immediately parseable by humans. No UUIDs, no opaque identifiers. - CLI friendly. Short enough to type; colon separator is unambiguous and shell-safe.
- Track context preserved. Unlike E-T format, unified IDs always carry track information, enabling agent routing and CEF pack selection.
- Collision-free cloud sync. Tenant/project/track path structure makes collisions mathematically impossible.
- Incremental adoption. Four-phase migration allows gradual transition with no breaking changes at any phase.
Negative
- Additional complexity. Project context resolution adds a fallback chain that must be understood and debugged.
- Registry maintenance. The project registry file must be kept in sync with actual projects. Stale entries cause validation failures.
- Hook performance. Loading and parsing
project-registry.jsonon every tool call adds latency. Mitigated by caching the registry in memory per session. - E-T deprecation timeline. Existing scripts and documentation referencing E-T format require updates during Phase 3-4.
Neutral
- Session log format change. Adding
PILOT:prefix to session logs is optional for PILOT project work, avoiding unnecessary noise. - Cloud ID opacity. Users never see the full
tenant_id/project_id/track.section.taskformat; it exists only in the sync layer.
Implementation Plan
Phase 1: Foundation (Week 1-2)
| Task | Component | Description |
|---|---|---|
| 1.1 | project-registry.json | Create initial registry with PILOT project |
| 1.2 | task-id-validator.py | Update regex to accept optional project prefix |
| 1.3 | resolve_project() | Implement project context resolution function in scripts/core/task_id.py |
| 1.4 | CODITECT_PROJECT | Document env var in CLAUDE.md and .env.example |
| 1.5 | Tests | Unit tests for new regex, resolution logic, registry validation |
Phase 2: Integration (Week 3-4)
| Task | Component | Description |
|---|---|---|
| 2.1 | task-tracking-enforcer.py | Update to pass project context to sync |
| 2.2 | cloud_sync_client.py | Add cloud_task_id generation with tenant prefix |
| 2.3 | work_items.py | Accept unified format alongside E-T format |
| 2.4 | work_item_bridge table | Create bridge table in sessions.db |
| 2.5 | /session-log command | Auto-prepend resolved project context |
| 2.6 | .coditect-project file | Add to PILOT repository root |
Phase 3: Adoption (Week 5-8)
| Task | Component | Description |
|---|---|---|
| 3.1 | Session logs | Add project prefix to new session log entries |
| 3.2 | work_items.py | Default new tasks to unified format |
| 3.3 | work_items.py | Show unified equivalent for E-T tasks |
| 3.4 | CLAUDE.md | Update Task ID Protocol section with unified format |
| 3.5 | STANDARD-TRACK-NOMENCLATURE | Update standard document |
| 3.6 | /cxq | Support project-scoped queries (/cxq --project PILOT "search") |
Phase 4: Deprecation (Week 9-12)
| Task | Component | Description |
|---|---|---|
| 4.1 | work_items.py | Emit deprecation warning for E-T format input |
| 4.2 | Migration script | Bulk-convert E-T references in sessions.db to unified format |
| 4.3 | Documentation | Remove E-T examples from all documentation |
| 4.4 | task-id-validator.py | Add optional E-T rejection mode (configurable) |
Migration Strategy
Existing Task ID Preservation
All existing task IDs in the following locations remain valid without modification:
| Location | Example | Status |
|---|---|---|
TRACK files (TRACK-A-*.md) | A.9.1.3 | Valid (resolves to PILOT:A.9.1.3) |
| Session logs | [A.9.1.3] | Valid (PILOT context assumed) |
| CLAUDE.md examples | A.9.1.3: | Valid (backward compatible regex) |
| Governance hooks | A.9.1.3: in descriptions | Valid (extended regex accepts bare format) |
| sessions.db task_tracking | E001-T001 | Valid (bridge table maps to unified) |
Data Migration
sessions.db: No schema changes required for existing tables. The work_item_bridge table is additive. Existing task_tracking rows with E-T format IDs gain a bridge mapping but are not modified in place.
Session logs: Historical session logs are not retroactively modified. Only new entries gain explicit project context when working outside PILOT.
Cloud sync: Existing synced tasks are re-synced with cloud task IDs on next sync cycle. The cloud API accepts both old (bare track ID) and new (tenant-prefixed) formats during the transition period.
Rollback Plan
If the unified format causes issues:
- Set
CODITECT_UNIFIED_TASK_ID=0environment variable to disable project prefix parsing task-id-validator.pyfalls back to ADR-054 bare format validationwork_items.pycontinues to accept E-T format without deprecation warnings- No data migration is destructive; bridge table can be dropped without data loss
Examples
Developer Working on PILOT (Default)
# No changes to existing workflow
export CODITECT_PROJECT=PILOT # Optional, this is the default
# Tool call description - bare format still works
Bash(description="A.9.1.3: Run database migration")
# Explicit prefix also works
Bash(description="PILOT:A.9.1.3: Run database migration")
# Session log entry
### 2026-02-02T14:30:00Z - [A.9.1.3] Database migration
Developer Working on Customer Project
# Set project context
export CODITECT_PROJECT=CUST-001
# Tool calls automatically scoped to CUST-001
Bash(description="A.3.1.2: Set up customer database schema")
# Validates: track A exists in CUST-001's registered tracks
# Cross-project reference requires explicit prefix
Bash(description="PILOT:H.8.1.6: Check framework dependency")
# Session log entry includes project context
### 2026-02-02T15:00:00Z - [CUST-001:A.3.1.2] Customer database setup
Cloud Sync Scenario
# Local task
PILOT:A.9.1.3
# Synced to cloud as
az1ai/PILOT/A.9.1.3
# Customer task
CUST-001:A.3.1.2
# Synced to cloud as
tenant_acme_001/CUST-001/A.3.1.2
# Query from cloud API
GET /api/v1/context/tasks/?cloud_task_id=az1ai/PILOT/A.9.1.3
GET /api/v1/context/tasks/?project=CUST-001&track=A
Work Item Bridge
# Legacy command
python3 scripts/work_items.py show E001-T001
# Output:
# E001-T001 (PILOT:A.9.1.3)
# Status: completed
# Track: A (Backend API)
# Project: PILOT
# Unified command
python3 scripts/work_items.py show PILOT:A.9.1.3
# Output:
# PILOT:A.9.1.3 (legacy: E001-T001)
# Status: completed
# Track: A (Backend API)
# Project: PILOT
Related ADRs
| ADR | Relationship |
|---|---|
| ADR-054 | Extended: Track nomenclature gains optional project prefix |
| ADR-056 | Extended: Action-level tracking gains project context |
| ADR-074 | Modified: Governance hooks updated for unified format |
| ADR-115 | Aligned: Hybrid task specification compatible with unified IDs |
| ADR-116 | Unchanged: Track-based plan architecture remains SSOT |
| ADR-117 | Extended: Hierarchical plan location gains project dimension |
| ADR-144 | Foundation: Project registry in org.db provides project metadata for task ID validation |
| ADR-145 | Extended: RBAC uses project prefix for project-scoped permission validation |
References
- ADR-054: Track Nomenclature Extensibility
- ADR-056: Action-Level Task Tracking
- ADR-074: Governance Hook Architecture
- ADR-115: Hybrid Task Specification Standard
- ADR-116: Track-Based Plan Architecture
- ADR-117: Hierarchical Plan Location Strategy
- ADR-144: Multi-Project Registry Architecture
- ADR-145: Project-Level RBAC & Multi-Tenant Access Control
- CODITECT-STANDARD-TRACK-NOMENCLATURE.md
- CODITECT-STANDARD-AUTOMATION.md (Principle #12)
hooks/task-id-validator.py- Current PreToolUse validatorhooks/task-tracking-enforcer.py- Current TodoWrite enforcerscripts/work_items.py- Current E-T format task managementscripts/core/cloud_sync_client.py- Cloud sync API client