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/:productionchannel 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/removesubcommand. This PR uses--profileoninit. 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-directivesslash 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=ioscommand would automateinit --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
| Scenario | Behavior |
|---|---|
era-code init in a fresh iOS repo, interactive | Prompts; detection suggests ios; user confirms; ios pack + all/ pack ship |
era-code init --profile=ios in a fresh repo | No prompt; ios pack + all/ pack ship |
era-code init --profile=bogus | Errors with the valid profile list |
era-code init in an existing core-service repo, no flag | Profile preserved; no surprise change |
era-code init --profile=ios in an existing core-service repo | Switches to ios; 060-upstream-deployment.md removed; ios pack installed |
era-code upgrade in a project with a profile field | Refreshes 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 flag | Defaults to core-service |
User edits .era/manifest.json to set profile: "tools-service" and runs era-code upgrade | Switches to tools-service pack; removes any core-service-only files |
Open questions for review
- Profile-name typo handling. A user typing
era-code init --profile=oisgets an error. Should we suggest the closest valid name (Levenshtein)? Cheap to add, easy to bikeshed. tools-servicepack contents. Today it's empty (placeholder). Tools-cluster services have their own conventions (Force=truesyncOption, no production overlay, hand-rolled CD) — worth a follow-up to extract these from060into a tools-specific pack.- iOS pack vs era-hub local directive. Once this lands, era-hub's local
100-ios.mdbecomes 80% redundant with the upstreamiospack. Plan: era-hub PR that swaps profile toios, removes local100-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. - Should we ship an
androidplaceholder profile now? See decision (1).