Skip to main content

Directive Profiles

Status: Proposed for review (PR #TBD) Author: Initial draft generated alongside the implementation Reviewers: CTO + eng

Problem

era-code ships universal upstream directives (verification, execution, context, edit-safety, quality, etc.) plus 060-upstream-deployment.md — which encodes the Era core platform's k8s/ArgoCD topology. That directive lands in every project that runs era-code init, including projects where it's noise:

  • iOS apps (no kustomize overlays, no ArgoCD, no :staging/:production channel tags)
  • Tools-cluster services (different branch/cluster mapping)
  • Libraries (no deployment topology at all)

This noise has two costs: agent context bloat (the agent reads rules that don't apply, wasting tokens and risking confused decisions) and an authoring gap (there's no place to ship platform-specific guidance like iOS TestFlight flow or Android Play Store conventions).

The trigger for this work: era-hub is iOS. Its CI/CD topology is main → internal-testflight → external-testflight → production with Fastlane Match and TestFlight promotion gating — a flow that has nothing to do with 060's ArgoCD/kustomize content. Yet 060 ships into era-hub on every era-code init.

Proposal

Introduce directive profiles: each project declares what it is, and era-code ships only the directives that match.

Source layout

src/templates/resources/directives/
├── all/ # always copied, regardless of profile
│ ├── 000-upstream-header.md
│ ├── 005-upstream-context-sync.md
│ ├── 010-upstream-verification.md
│ ├── 020-upstream-execution.md
│ ├── 030-upstream-context.md
│ ├── 040-upstream-edit-safety.md
│ ├── 045-upstream-time-awareness.md
│ ├── 050-upstream-quality.md
│ ├── 070-upstream-git-discipline.md
│ └── 999-upstream-footer.md
└── profiles/
├── core-service/ # k8s/ArgoCD backend services
│ └── 060-upstream-deployment.md
├── tools-service/ # placeholder — empty pack today
├── ios/ # Apple platform apps
│ ├── 100-ios-style.md
│ ├── 110-ios-xcode.md
│ ├── 120-ios-dependencies.md
│ └── 130-ios-testflight-flow.md
└── library/ # placeholder — universal pack only

Manifest schema

.era/manifest.json gains an optional profile field:

{
"version": "3.10.0",
"initialized": "...",
"lastUpdated": "...",
"tools": ["opencode"],
"profile": "ios",
"features": { ... }
}

Valid values: core-service (default) | tools-service | ios | library.

Backward compatibility: manifests without profile are read as core-service — matches the historical behavior (every project got 060).

CLI surface

# Explicit
era-code init --profile=ios

# Interactive (no flag, not --quiet/--json/--skip-prompts)
# → prompts with auto-detection seeding the default:
# - .xcodeproj or Fastfile present → suggests ios
# - k8s/overlays/staging|production → suggests core-service
# - k8s/overlays/tools → suggests tools-service
# - Package.swift only → suggests library
# - otherwise → suggests core-service

# Non-interactive without flag
era-code init --skip-prompts # uses existing manifest profile, or core-service

era-code upgrade reads profile from the manifest, copies all/* + profiles/<profile>/* into .era/memory/directives/, and removes any upstream-managed files no longer in scope (e.g. switching from core-service to ios drops 060-upstream-deployment.md). User-authored directives (anything not in the templates registry) are preserved.

Decisions taken (please review)

1. Closed list of profile names

Profile names are a closed enum: core-service | tools-service | ios | library. New profiles require an era-code change.

Why closed: stops every team from inventing their own profile name and fragmenting the conventions. New profiles should be deliberate.

Open question for CTO: is android worth adding now as a placeholder, even if the pack is empty? Symmetry with ios would be nice; YAGNI says wait.

2. Default for existing repos is core-service

Manifests without a profile field are read as core-service. The first era-code upgrade after this lands will install 060 (no change for existing core repos) and continue working as before. iOS repos (era-hub) need to opt in by running era-code init --profile=ios.

Why: preserves the pre-profile behavior for the majority of existing repos (most are core services). Surprising 30+ repos by changing what ships is worse than asking 1-2 iOS repos to opt in.

Alternative considered: prompt every existing repo on first upgrade. Rejected — too noisy, and the answer is core-service for everything except iOS.

3. Single profile per repo, not multi-profile

A project picks ONE profile. No profiles: ["ios", "library"].

Why: simpler to reason about, simpler to filter, and the failure mode for "I picked iOS but I also have a Node tool inside" is to author a local override directive in the repo — which is already supported via /era-directives.

Alternative considered: multi-profile with order-based merge. Rejected — opens the question of "what if two profiles disagree on branch flow", which is rare but ugly when it happens.

4. Hard-remove out-of-scope directives on sync

When switching profiles (or when an upstream pack drops a file), era-code upgrade deletes the now-out-of-scope file from .era/memory/directives/.

Why: stale directives are worse than missing directives — the agent reads them as authoritative. Quarantine to _archived/ was considered, but that just moves the problem (the agent doesn't read the archived dir, so the file is dead weight on disk).

Safety: only files that match a known template-managed filename (anything ever shipped by all/ or any profiles/*/) are eligible for removal. User-authored directives (e.g. 100-team-conventions.md) are never touched.

Alternative considered: prompt before removing. Rejected — would happen on every era-code upgrade after a profile change, and the answer is always "yes, remove it".

5. Detection is suggestion-only, never silent

suggestProfile looks at filesystem signals (.xcodeproj, k8s/overlays/, Package.swift, fastlane/) and seeds the prompt's default. It NEVER picks the profile silently.

Why: monorepos with multiple signal types (e.g. era-hub has Watch + Widget + EraShared package + plans for backend integration) shouldn't get a silently-wrong choice. Explicit user confirmation is cheap.

Out of scope (follow-ups)

  • era-code platform add/remove subcommand. This PR uses --profile on init. A dedicated subcommand would be cleaner ergonomics but is purely additive; deferred.
  • Profile packs for additional platforms. Android, server-side ML, embedded firmware — easy to add once the structure is in place. None ship in this PR.
  • /era-directives slash command awareness. The slash command could suggest profile-appropriate categories when the user authors a new directive. Out of scope for this PR.
  • Migration tooling. A era-code migrate --to-profile=ios command would automate init --profile=ios + clean-up of any directives the user shouldn't be carrying. Not built; the manual path (era-code init --profile=ios + review of removed files) is sufficient.

Behavior matrix

ScenarioBehavior
era-code init in a fresh iOS repo, interactivePrompts; detection suggests ios; user confirms; ios pack + all/ pack ship
era-code init --profile=ios in a fresh repoNo prompt; ios pack + all/ pack ship
era-code init --profile=bogusErrors with the valid profile list
era-code init in an existing core-service repo, no flagProfile preserved; no surprise change
era-code init --profile=ios in an existing core-service repoSwitches to ios; 060-upstream-deployment.md removed; ios pack installed
era-code upgrade in a project with a profile fieldRefreshes upstream + applies profile filter
era-code upgrade in a project WITHOUT profile (legacy)Treats as core-service; no behavior change
era-code init --skip-prompts in a fresh repo, no flagDefaults to core-service
User edits .era/manifest.json to set profile: "tools-service" and runs era-code upgradeSwitches to tools-service pack; removes any core-service-only files

Open questions for review

  1. Profile-name typo handling. A user typing era-code init --profile=ois gets an error. Should we suggest the closest valid name (Levenshtein)? Cheap to add, easy to bikeshed.
  2. tools-service pack contents. Today it's empty (placeholder). Tools-cluster services have their own conventions (Force=true syncOption, no production overlay, hand-rolled CD) — worth a follow-up to extract these from 060 into a tools-specific pack.
  3. iOS pack vs era-hub local directive. Once this lands, era-hub's local 100-ios.md becomes 80% redundant with the upstream ios pack. Plan: era-hub PR that swaps profile to ios, removes local 100-ios.md, keeps only era-hub-specific bits (specific bundle IDs, "Beta Testers" group name, MATCH config). That migration is a follow-up PR, not part of this one.
  4. Should we ship an android placeholder profile now? See decision (1).