agentic-team 0.0.0-stage → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +189 -2
- package/bin/build-template.js +208 -0
- package/bin/index.js +1334 -0
- package/bin/platforms.js +510 -0
- package/package.json +41 -4
- package/template/.claude-plugin/plugin.json +10 -0
- package/template/.orchestra/README.md +391 -0
- package/template/.orchestra/blueprints/README.md +87 -0
- package/template/.orchestra/blueprints/api-only.md +46 -0
- package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
- package/template/.orchestra/blueprints/saas-starter.md +69 -0
- package/template/.orchestra/config.yml +293 -0
- package/template/.orchestra/roles/orchestrator.md +105 -0
- package/template/.orchestra/roles/product-manager.md +439 -0
- package/template/.orchestra/scripts/compact-diff.awk +45 -0
- package/template/.orchestra/scripts/context.mjs +373 -0
- package/template/.orchestra/stacks/README.md +81 -0
- package/template/.orchestra/stacks/_lib/common.sh +3095 -0
- package/template/.orchestra/stacks/_repo/setup.sh +1113 -0
- package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
- package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
- package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
- package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
- package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
- package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
- package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
- package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
- package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
- package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
- package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
- package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
- package/template/.orchestra/trackers/github.sh +467 -0
- package/template/CLAUDE.md +54 -0
- package/template/agents/architect.md +83 -0
- package/template/agents/engineer-high.md +17 -0
- package/template/agents/engineer-low.md +17 -0
- package/template/agents/engineer-max.md +17 -0
- package/template/agents/engineer-medium.md +17 -0
- package/template/agents/engineer-xhigh.md +17 -0
- package/template/agents/lead.md +925 -0
- package/template/agents/milestone-reviewer.md +65 -0
- package/template/agents/orchestrator.md +105 -0
- package/template/agents/product-manager.md +439 -0
- package/template/agents/re-reviewer.md +25 -0
- package/template/agents/reviewer.md +303 -0
- package/template/agents/scaffolder.md +206 -0
- package/template/agents/tracker.md +202 -0
- package/template/commands/blueprint.md +23 -0
- package/template/commands/create-role.md +64 -0
- package/template/commands/export.md +45 -0
- package/template/commands/help.md +56 -0
- package/template/commands/hotfix.md +14 -0
- package/template/commands/orchestrator.md +12 -0
- package/template/commands/pm.md +18 -0
- package/template/commands/rewind.md +63 -0
- package/template/commands/setup.md +15 -0
- package/template/commands/start.md +29 -0
- package/template/commands/status.md +11 -0
- package/template/commands/sync.md +76 -0
- package/template/commands/verifier.md +53 -0
- package/template/hooks/audit-context.sh +166 -0
- package/template/hooks/enforce-role.sh +193 -0
- package/template/hooks/protect-system-files.sh +123 -0
- package/template/hooks/review-rounds.sh +172 -0
- package/template/rules/acceptance-check.orchestra.md +11 -0
- package/template/rules/analytics-kpi.orchestra.md +56 -0
- package/template/rules/code-standards.orchestra.md +35 -0
- package/template/rules/comments.orchestra.md +77 -0
- package/template/rules/commit-format.orchestra.md +20 -0
- package/template/rules/dependency-upgrade.orchestra.md +112 -0
- package/template/rules/design-parity.orchestra.md +181 -0
- package/template/rules/observability.orchestra.md +121 -0
- package/template/rules/performance.orchestra.md +190 -0
- package/template/rules/phase-limits.orchestra.md +3 -0
- package/template/rules/pull-request.orchestra.md +218 -0
- package/template/rules/role-boundaries.orchestra.md +132 -0
- package/template/rules/security.orchestra.md +106 -0
- package/template/rules/stuck-detection.orchestra.md +25 -0
- package/template/rules/testing-standards.orchestra.md +219 -0
- package/template/rules/tracker-authority.orchestra.md +198 -0
- package/template/rules/verification-gate.orchestra.md +56 -0
- package/template/skills/accessibility/SKILL.md +31 -0
- package/template/skills/android-testing/SKILL.md +112 -0
- package/template/skills/architecture/SKILL.md +175 -0
- package/template/skills/auth-setup/SKILL.md +105 -0
- package/template/skills/best-practices/SKILL.md +29 -0
- package/template/skills/code-optimizer/SKILL.md +27 -0
- package/template/skills/core-web-vitals/SKILL.md +32 -0
- package/template/skills/crud-api/SKILL.md +32 -0
- package/template/skills/debug/SKILL.md +28 -0
- package/template/skills/deployment/SKILL.md +104 -0
- package/template/skills/error-handling/SKILL.md +154 -0
- package/template/skills/frontend-design/SKILL.md +30 -0
- package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
- package/template/skills/ios-testing/SKILL.md +100 -0
- package/template/skills/mobile-testing/SKILL.md +102 -0
- package/template/skills/observability/SKILL.md +91 -0
- package/template/skills/project-structure/SKILL.md +114 -0
- package/template/skills/push-notifications/SKILL.md +146 -0
- package/template/skills/react-best-practices/SKILL.md +31 -0
- package/template/skills/realtime/SKILL.md +146 -0
- package/template/skills/review/SKILL.md +82 -0
- package/template/skills/testing/SKILL.md +45 -0
- package/template/skills/web-quality-audit/SKILL.md +47 -0
- package/template/skills/web-testing/SKILL.md +120 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Native iOS — SwiftUI, generated from an XcodeGen manifest.
|
|
3
|
+
#
|
|
4
|
+
# The project is generated from project.yml, never hand-edited as a .pbxproj.
|
|
5
|
+
# A binary project file cannot be reviewed in a diff and corrupts under
|
|
6
|
+
# concurrent edits — both fatal for an agent-driven workflow.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
DIR="${1:?usage: setup.sh <target-dir>}"
|
|
10
|
+
RAW_NAME="${2:-$(basename "$DIR")}"
|
|
11
|
+
# Swift type names cannot contain dashes.
|
|
12
|
+
NAME="$(printf '%s' "$RAW_NAME" | sed 's/[^A-Za-z0-9]//g')"
|
|
13
|
+
[ -n "$NAME" ] || NAME="App"
|
|
14
|
+
|
|
15
|
+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../_lib" && pwd)"
|
|
16
|
+
# shellcheck source=../_lib/common.sh
|
|
17
|
+
source "$LIB/common.sh"
|
|
18
|
+
|
|
19
|
+
command -v xcodegen >/dev/null 2>&1 || die "xcodegen not found. Install it first: brew install xcodegen"
|
|
20
|
+
command -v xcodebuild >/dev/null 2>&1 || die "xcodebuild not found. Install Xcode, then: xcode-select --install"
|
|
21
|
+
|
|
22
|
+
# Resolved now, from each project's latest release. A version written from
|
|
23
|
+
# memory is the single most reliable way to waste a debugging cycle.
|
|
24
|
+
resolve_version() {
|
|
25
|
+
local repo="$1" fallback="$2" tag
|
|
26
|
+
tag="$(curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null \
|
|
27
|
+
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(String(JSON.parse(s).tag_name||""))}catch{}})' 2>/dev/null || true)"
|
|
28
|
+
tag="${tag#v}"
|
|
29
|
+
case "$tag" in
|
|
30
|
+
''|*[!0-9.]*) echo "$fallback" ;;
|
|
31
|
+
*) echo "$tag" ;;
|
|
32
|
+
esac
|
|
33
|
+
}
|
|
34
|
+
SENTRY_VERSION="$(resolve_version getsentry/sentry-cocoa 9.26.0)"
|
|
35
|
+
POSTHOG_VERSION="$(resolve_version PostHog/posthog-ios 3.69.10)"
|
|
36
|
+
log "sentry-cocoa ${SENTRY_VERSION}, posthog-ios ${POSTHOG_VERSION}"
|
|
37
|
+
|
|
38
|
+
log "scaffolding native iOS into $DIR (target $NAME)"
|
|
39
|
+
mkdir -p "$DIR"/Sources/{App,DesignSystem,Observability} "$DIR"/Tests/DesignSystemTests "$DIR"/UITests "$DIR"/Resources
|
|
40
|
+
cd "$DIR"
|
|
41
|
+
|
|
42
|
+
cat > project.yml <<YML
|
|
43
|
+
name: ${NAME}
|
|
44
|
+
options:
|
|
45
|
+
bundleIdPrefix: com.example
|
|
46
|
+
deploymentTarget:
|
|
47
|
+
iOS: "17.0"
|
|
48
|
+
# Resolved from each project's releases at scaffold time, never recalled.
|
|
49
|
+
# Pinned to a major with upToNextMajor so a patch arrives without a decision
|
|
50
|
+
# and a breaking change does not.
|
|
51
|
+
packages:
|
|
52
|
+
Sentry:
|
|
53
|
+
url: https://github.com/getsentry/sentry-cocoa
|
|
54
|
+
majorVersion: ${SENTRY_VERSION}
|
|
55
|
+
PostHog:
|
|
56
|
+
url: https://github.com/PostHog/posthog-ios
|
|
57
|
+
majorVersion: ${POSTHOG_VERSION}
|
|
58
|
+
targets:
|
|
59
|
+
${NAME}:
|
|
60
|
+
type: application
|
|
61
|
+
platform: iOS
|
|
62
|
+
sources: [Sources]
|
|
63
|
+
resources: [Resources]
|
|
64
|
+
dependencies:
|
|
65
|
+
- package: Sentry
|
|
66
|
+
- package: PostHog
|
|
67
|
+
info:
|
|
68
|
+
path: Resources/Info.plist
|
|
69
|
+
properties:
|
|
70
|
+
UILaunchScreen: {}
|
|
71
|
+
# Fed from build settings, which are fed from the environment. Absent
|
|
72
|
+
# in a checkout with no credentials, and the app says so rather than
|
|
73
|
+
# crashing or reporting nowhere in silence.
|
|
74
|
+
SentryDSN: \$(SENTRY_DSN)
|
|
75
|
+
PostHogAPIKey: \$(POSTHOG_API_KEY)
|
|
76
|
+
PostHogHost: \$(POSTHOG_HOST)
|
|
77
|
+
AppEnvironment: \$(APP_ENVIRONMENT)
|
|
78
|
+
settings:
|
|
79
|
+
base:
|
|
80
|
+
SWIFT_STRICT_CONCURRENCY: complete
|
|
81
|
+
GENERATE_INFOPLIST_FILE: NO
|
|
82
|
+
SENTRY_DSN: \$(SENTRY_DSN)
|
|
83
|
+
POSTHOG_API_KEY: \$(POSTHOG_API_KEY)
|
|
84
|
+
POSTHOG_HOST: \$(POSTHOG_HOST)
|
|
85
|
+
APP_ENVIRONMENT: \$(APP_ENVIRONMENT)
|
|
86
|
+
${NAME}Tests:
|
|
87
|
+
type: bundle.unit-test
|
|
88
|
+
platform: iOS
|
|
89
|
+
sources: [Tests]
|
|
90
|
+
settings:
|
|
91
|
+
base:
|
|
92
|
+
# The test bundle needs its own Info.plist to code sign. Generating
|
|
93
|
+
# it is correct here — only the app target ships a real one.
|
|
94
|
+
GENERATE_INFOPLIST_FILE: YES
|
|
95
|
+
dependencies:
|
|
96
|
+
- target: ${NAME}
|
|
97
|
+
${NAME}UITests:
|
|
98
|
+
type: bundle.ui-testing
|
|
99
|
+
platform: iOS
|
|
100
|
+
sources: [UITests]
|
|
101
|
+
settings:
|
|
102
|
+
base:
|
|
103
|
+
GENERATE_INFOPLIST_FILE: YES
|
|
104
|
+
dependencies:
|
|
105
|
+
- target: ${NAME}
|
|
106
|
+
# xcodegen does generate a scheme covering both test bundles on its own, so
|
|
107
|
+
# this is not a fix for anything currently broken. It is written down because
|
|
108
|
+
# the alternative is depending on a default: which targets a generated scheme
|
|
109
|
+
# includes is xcodegen's choice, and a change there would silently stop the
|
|
110
|
+
# UI tests running while the gate kept reporting green.
|
|
111
|
+
schemes:
|
|
112
|
+
${NAME}:
|
|
113
|
+
build:
|
|
114
|
+
targets:
|
|
115
|
+
${NAME}: all
|
|
116
|
+
test:
|
|
117
|
+
targets:
|
|
118
|
+
- ${NAME}Tests
|
|
119
|
+
- ${NAME}UITests
|
|
120
|
+
YML
|
|
121
|
+
|
|
122
|
+
cat > Resources/Info.plist <<'PLIST'
|
|
123
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
124
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
125
|
+
<plist version="1.0">
|
|
126
|
+
<dict>
|
|
127
|
+
<key>UILaunchScreen</key>
|
|
128
|
+
<dict/>
|
|
129
|
+
</dict>
|
|
130
|
+
</plist>
|
|
131
|
+
PLIST
|
|
132
|
+
|
|
133
|
+
cat > Sources/App/App.swift <<SWIFT
|
|
134
|
+
import SwiftUI
|
|
135
|
+
|
|
136
|
+
@main
|
|
137
|
+
struct ${NAME}App: App {
|
|
138
|
+
init() {
|
|
139
|
+
// Before the first view, so a crash during startup is still reported.
|
|
140
|
+
Observability.start()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var body: some Scene {
|
|
144
|
+
WindowGroup {
|
|
145
|
+
ContentView()
|
|
146
|
+
// Every screen reports a view. At the screen, not in a
|
|
147
|
+
// wrapper three layers away that eventually fires for the
|
|
148
|
+
// wrong reason.
|
|
149
|
+
.onAppear { analytics.capture(.screenViewed(name: "home")) }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private let analytics: AnalyticsSink = PostHogSink()
|
|
155
|
+
|
|
156
|
+
struct ContentView: View {
|
|
157
|
+
var body: some View {
|
|
158
|
+
Text("${NAME}")
|
|
159
|
+
.padding(Tokens.Spacing.gutter)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
SWIFT
|
|
163
|
+
|
|
164
|
+
mkdir -p Sources/Observability Tests/ObservabilityTests
|
|
165
|
+
|
|
166
|
+
cat > Sources/Observability/Events.swift <<'SWIFT'
|
|
167
|
+
import Foundation
|
|
168
|
+
|
|
169
|
+
/// Every analytics event this app can send, and its payload.
|
|
170
|
+
///
|
|
171
|
+
/// One place, and a closed enum rather than strings at call sites. A string
|
|
172
|
+
/// literal is how analytics quietly rots: the name is changed here, the call
|
|
173
|
+
/// site still compiles, and the funnel goes silent with nobody the wiser.
|
|
174
|
+
///
|
|
175
|
+
/// Naming is `object_action` and never mixes styles. See
|
|
176
|
+
/// .claude/rules/analytics-kpi.orchestra.md.
|
|
177
|
+
public enum AnalyticsEvent: Equatable, Sendable {
|
|
178
|
+
/// Every screen reports a view. `name` is the screen, not the file.
|
|
179
|
+
case screenViewed(name: String)
|
|
180
|
+
/// A critical flow starting, succeeding, and failing — with a reason.
|
|
181
|
+
/// A funnel that only records success cannot say why people left.
|
|
182
|
+
case flowStarted(flow: String)
|
|
183
|
+
case flowCompleted(flow: String)
|
|
184
|
+
case flowFailed(flow: String, reason: String)
|
|
185
|
+
|
|
186
|
+
public var name: String {
|
|
187
|
+
switch self {
|
|
188
|
+
case .screenViewed: return "screen_viewed"
|
|
189
|
+
case .flowStarted: return "flow_started"
|
|
190
|
+
case .flowCompleted: return "flow_completed"
|
|
191
|
+
case .flowFailed: return "flow_failed"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// No PII. No email, no name, nothing the user typed — the reason on a
|
|
196
|
+
/// failure is a code, not a message.
|
|
197
|
+
public var properties: [String: String] {
|
|
198
|
+
switch self {
|
|
199
|
+
case let .screenViewed(name): return ["screen": name]
|
|
200
|
+
case let .flowStarted(flow): return ["flow": flow]
|
|
201
|
+
case let .flowCompleted(flow): return ["flow": flow]
|
|
202
|
+
case let .flowFailed(flow, reason): return ["flow": flow, "reason": reason]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// The seam. Production sends to PostHog; a test captures instead.
|
|
208
|
+
public protocol AnalyticsSink: Sendable {
|
|
209
|
+
func capture(_ event: AnalyticsEvent)
|
|
210
|
+
}
|
|
211
|
+
SWIFT
|
|
212
|
+
|
|
213
|
+
cat > Sources/Observability/Observability.swift <<'SWIFT'
|
|
214
|
+
import Foundation
|
|
215
|
+
import PostHog
|
|
216
|
+
import Sentry
|
|
217
|
+
|
|
218
|
+
/// Error monitoring and analytics, started once at launch.
|
|
219
|
+
///
|
|
220
|
+
/// Both are configured from Info.plist rather than hardcoded, and both are
|
|
221
|
+
/// skipped when their key is absent — a checkout with no credentials has to
|
|
222
|
+
/// build, run and test, or nobody can work on the app without production
|
|
223
|
+
/// secrets. Absent is logged, not silent: see
|
|
224
|
+
/// .claude/rules/observability.orchestra.md.
|
|
225
|
+
public enum Observability {
|
|
226
|
+
/// Set by the build. Release tagging is required — an error you cannot
|
|
227
|
+
/// tie to a build is an error you cannot fix.
|
|
228
|
+
public static var release: String {
|
|
229
|
+
let info = Bundle.main.infoDictionary
|
|
230
|
+
let version = info?["CFBundleShortVersionString"] as? String ?? "0"
|
|
231
|
+
let build = info?["CFBundleVersion"] as? String ?? "0"
|
|
232
|
+
return "\(version)+\(build)"
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private static func setting(_ key: String) -> String? {
|
|
236
|
+
guard let value = Bundle.main.object(forInfoDictionaryKey: key) as? String,
|
|
237
|
+
!value.isEmpty,
|
|
238
|
+
// xcconfig substitutes an empty variable as the literal name.
|
|
239
|
+
!value.hasPrefix("$(")
|
|
240
|
+
else { return nil }
|
|
241
|
+
return value
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public static func start() {
|
|
245
|
+
if let dsn = setting("SentryDSN") {
|
|
246
|
+
SentrySDK.start { options in
|
|
247
|
+
options.dsn = dsn
|
|
248
|
+
options.releaseName = release
|
|
249
|
+
options.environment = setting("AppEnvironment") ?? "development"
|
|
250
|
+
// Scrub before send, not after. A crash report is not a place
|
|
251
|
+
// for anything the user typed.
|
|
252
|
+
options.beforeSend = { event in
|
|
253
|
+
event.user?.email = nil
|
|
254
|
+
event.user?.username = nil
|
|
255
|
+
event.user?.ipAddress = nil
|
|
256
|
+
return event
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
print("[observability] no SentryDSN in Info.plist — crash reporting is off")
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if let key = setting("PostHogAPIKey") {
|
|
264
|
+
let config = PostHogConfig(projectToken: key, host: setting("PostHogHost") ?? "https://eu.i.posthog.com")
|
|
265
|
+
// Identify with a stable anonymous id; never with an email.
|
|
266
|
+
config.personProfiles = .identifiedOnly
|
|
267
|
+
PostHogSDK.shared.setup(config)
|
|
268
|
+
} else {
|
|
269
|
+
print("[observability] no PostHogAPIKey in Info.plist — analytics is off")
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Sends to PostHog when it is configured, and nowhere when it is not.
|
|
275
|
+
public struct PostHogSink: AnalyticsSink {
|
|
276
|
+
public init() {}
|
|
277
|
+
public func capture(_ event: AnalyticsEvent) {
|
|
278
|
+
PostHogSDK.shared.capture(event.name, properties: event.properties)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
SWIFT
|
|
282
|
+
|
|
283
|
+
cat > Tests/ObservabilityTests/EventsTests.swift <<'SWIFT'
|
|
284
|
+
import XCTest
|
|
285
|
+
@testable import __NAME__
|
|
286
|
+
|
|
287
|
+
/// Instrumentation is code, so it is tested like code.
|
|
288
|
+
///
|
|
289
|
+
/// The sink is the seam: this drives the real event type through a capturing
|
|
290
|
+
/// implementation, so a renamed case or a changed payload fails here rather
|
|
291
|
+
/// than in a dashboard three weeks later.
|
|
292
|
+
final class CapturingSink: AnalyticsSink, @unchecked Sendable {
|
|
293
|
+
private(set) var captured: [AnalyticsEvent] = []
|
|
294
|
+
func capture(_ event: AnalyticsEvent) { captured.append(event) }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
final class EventsTests: XCTestCase {
|
|
298
|
+
func testScreenViewFiresOnceWithTheScreenName() {
|
|
299
|
+
let sink = CapturingSink()
|
|
300
|
+
sink.capture(.screenViewed(name: "home"))
|
|
301
|
+
|
|
302
|
+
XCTAssertEqual(sink.captured.count, 1)
|
|
303
|
+
XCTAssertEqual(sink.captured.first?.name, "screen_viewed")
|
|
304
|
+
XCTAssertEqual(sink.captured.first?.properties, ["screen": "home"])
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
func testEveryEventNameIsObjectAction() {
|
|
308
|
+
// Boring and consistent, decided once. A mixed convention is
|
|
309
|
+
// unqueryable six months later.
|
|
310
|
+
let events: [AnalyticsEvent] = [
|
|
311
|
+
.screenViewed(name: "home"),
|
|
312
|
+
.flowStarted(flow: "signup"),
|
|
313
|
+
.flowCompleted(flow: "signup"),
|
|
314
|
+
.flowFailed(flow: "signup", reason: "network"),
|
|
315
|
+
]
|
|
316
|
+
for event in events {
|
|
317
|
+
XCTAssertTrue(
|
|
318
|
+
event.name.contains("_"),
|
|
319
|
+
"\(event.name) is not object_action"
|
|
320
|
+
)
|
|
321
|
+
XCTAssertEqual(event.name, event.name.lowercased())
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
func testAFailureCarriesAReasonCode() {
|
|
326
|
+
// A funnel that only records success cannot tell you why people left,
|
|
327
|
+
// which is the only thing you actually wanted to know.
|
|
328
|
+
let event = AnalyticsEvent.flowFailed(flow: "checkout", reason: "card_declined")
|
|
329
|
+
XCTAssertEqual(event.properties["reason"], "card_declined")
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
func testNoEventCarriesFreeTextTheUserTyped() {
|
|
333
|
+
// Property values are identifiers, not sentences. The check is crude
|
|
334
|
+
// on purpose: a space is the cheapest signal that something typed has
|
|
335
|
+
// leaked into a property.
|
|
336
|
+
let events: [AnalyticsEvent] = [
|
|
337
|
+
.screenViewed(name: "home"),
|
|
338
|
+
.flowFailed(flow: "checkout", reason: "card_declined"),
|
|
339
|
+
]
|
|
340
|
+
for event in events {
|
|
341
|
+
for (key, value) in event.properties {
|
|
342
|
+
XCTAssertFalse(value.contains(" "), "\(key) looks like free text: \(value)")
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
SWIFT
|
|
348
|
+
sed -i.bak "s/__NAME__/${NAME}/" Tests/ObservabilityTests/EventsTests.swift
|
|
349
|
+
rm -f Tests/ObservabilityTests/EventsTests.swift.bak
|
|
350
|
+
|
|
351
|
+
cat > Sources/DesignSystem/Tokens.swift <<'SWIFT'
|
|
352
|
+
import SwiftUI
|
|
353
|
+
|
|
354
|
+
/// Design tokens. Populated from the Claude Design source before any
|
|
355
|
+
/// component is written. Color is one axis of several — do not stop there.
|
|
356
|
+
public enum Tokens {
|
|
357
|
+
public enum Spacing {
|
|
358
|
+
public static let gutter: CGFloat = 16
|
|
359
|
+
}
|
|
360
|
+
public enum Radius {
|
|
361
|
+
public static let control: CGFloat = 8
|
|
362
|
+
public static let surface: CGFloat = 12
|
|
363
|
+
}
|
|
364
|
+
public enum Motion {
|
|
365
|
+
public static let fast: Double = 0.12
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
SWIFT
|
|
369
|
+
|
|
370
|
+
cat > Tests/DesignSystemTests/TokensTests.swift <<SWIFT
|
|
371
|
+
import XCTest
|
|
372
|
+
@testable import ${NAME}
|
|
373
|
+
|
|
374
|
+
final class TokensTests: XCTestCase {
|
|
375
|
+
func testSpacingScaleIsPositive() {
|
|
376
|
+
XCTAssertGreaterThan(Tokens.Spacing.gutter, 0)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
func testControlRadiusIsSmallerThanSurface() {
|
|
380
|
+
XCTAssertLessThan(Tokens.Radius.control, Tokens.Radius.surface)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
SWIFT
|
|
384
|
+
|
|
385
|
+
# The highest-value single check on iOS: the app launches.
|
|
386
|
+
#
|
|
387
|
+
# No XCTest sees a missing Info.plist key, an entitlement the profile lacks,
|
|
388
|
+
# a @main that throws before the first view, or a resource that was never
|
|
389
|
+
# copied. The unit suite is green and the app dies on the splash screen.
|
|
390
|
+
cat > UITests/LaunchTests.swift <<'SWIFT'
|
|
391
|
+
import XCTest
|
|
392
|
+
|
|
393
|
+
final class LaunchTests: XCTestCase {
|
|
394
|
+
override func setUp() {
|
|
395
|
+
super.setUp()
|
|
396
|
+
continueAfterFailure = false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
func testAppLaunches() {
|
|
400
|
+
let app = XCUIApplication()
|
|
401
|
+
app.launch()
|
|
402
|
+
|
|
403
|
+
// Not a screenshot and not a copy match — either would need editing
|
|
404
|
+
// the day the design changes. This asserts the app reached a running
|
|
405
|
+
// state with a rendered window.
|
|
406
|
+
XCTAssertTrue(
|
|
407
|
+
app.wait(for: .runningForeground, timeout: 30),
|
|
408
|
+
"the app did not reach the foreground"
|
|
409
|
+
)
|
|
410
|
+
XCTAssertTrue(app.windows.firstMatch.exists, "no window was rendered")
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
SWIFT
|
|
414
|
+
|
|
415
|
+
cat > .swift-format <<'JSON'
|
|
416
|
+
{
|
|
417
|
+
"indentation": { "spaces": 4 },
|
|
418
|
+
"lineLength": 100
|
|
419
|
+
}
|
|
420
|
+
JSON
|
|
421
|
+
|
|
422
|
+
cat > verify.sh <<'V'
|
|
423
|
+
#!/usr/bin/env bash
|
|
424
|
+
# Normalised verification gate. Orchestra runs this instead of the npm
|
|
425
|
+
# commands in config.yml, because this app is not npm-based.
|
|
426
|
+
set -euo pipefail
|
|
427
|
+
|
|
428
|
+
proj="$(find . -maxdepth 1 -name '*.xcodeproj' | head -1)"
|
|
429
|
+
[ -n "$proj" ] || { echo "no .xcodeproj here — run 'xcodegen generate' first" >&2; exit 1; }
|
|
430
|
+
SCHEME="${SCHEME:-$(basename "$proj" .xcodeproj)}"
|
|
431
|
+
DEST="${DEST:-generic/platform=iOS Simulator}"
|
|
432
|
+
|
|
433
|
+
echo "── build"
|
|
434
|
+
xcodebuild build -scheme "$SCHEME" -destination "$DEST" -quiet
|
|
435
|
+
|
|
436
|
+
echo "── test"
|
|
437
|
+
# Resolve a concrete simulator by UDID. Grepping the human-readable device
|
|
438
|
+
# list yields names with trailing whitespace, which xcodebuild rejects with
|
|
439
|
+
# "Unable to find a device matching the provided destination specifier".
|
|
440
|
+
if [ -n "${SIM_UDID:-}" ]; then
|
|
441
|
+
udid="$SIM_UDID"
|
|
442
|
+
else
|
|
443
|
+
udid="$(xcrun simctl list devices available -j | node -e '
|
|
444
|
+
let s = "";
|
|
445
|
+
process.stdin.on("data", (d) => (s += d)).on("end", () => {
|
|
446
|
+
const j = JSON.parse(s);
|
|
447
|
+
const all = Object.values(j.devices).flat()
|
|
448
|
+
.filter((d) => d.isAvailable && /^iPhone/.test(d.name));
|
|
449
|
+
process.stdout.write(all[0] ? all[0].udid : "");
|
|
450
|
+
});
|
|
451
|
+
')"
|
|
452
|
+
fi
|
|
453
|
+
[ -n "$udid" ] || { echo "no iPhone simulator available — set SIM_UDID" >&2; exit 1; }
|
|
454
|
+
xcodebuild test -scheme "$SCHEME" -destination "id=$udid" -quiet
|
|
455
|
+
|
|
456
|
+
echo "── format"
|
|
457
|
+
if command -v swift-format >/dev/null 2>&1; then
|
|
458
|
+
swift-format lint -r Sources Tests
|
|
459
|
+
else
|
|
460
|
+
echo "swift-format not installed — skipped"
|
|
461
|
+
fi
|
|
462
|
+
V
|
|
463
|
+
chmod +x verify.sh
|
|
464
|
+
|
|
465
|
+
xcodegen generate
|
|
466
|
+
|
|
467
|
+
log "mobile-ios-native ready in $DIR"
|
|
468
|
+
warn "verification runs through ./verify.sh, not npm — config.yml reflects that"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Mobile — Native iOS (Swift)
|
|
2
|
+
|
|
3
|
+
## Who you are
|
|
4
|
+
|
|
5
|
+
You are a **senior native iOS engineer**. Not a web developer writing
|
|
6
|
+
Swift. You know the platform's conventions and you follow them, because an
|
|
7
|
+
app that ignores them feels wrong to users in ways they cannot name.
|
|
8
|
+
|
|
9
|
+
You think in value types, you understand the main actor, and you treat
|
|
10
|
+
retain cycles and unstructured concurrency as bugs you catch yourself —
|
|
11
|
+
not things review finds for you.
|
|
12
|
+
|
|
13
|
+
SOLID, KISS, YAGNI. No overengineering: no protocol with one conformer, no
|
|
14
|
+
coordinator for two screens.
|
|
15
|
+
|
|
16
|
+
## Project generation
|
|
17
|
+
|
|
18
|
+
The Xcode project is **generated from `project.yml` by XcodeGen**. Never
|
|
19
|
+
hand-edit a `.pbxproj`.
|
|
20
|
+
|
|
21
|
+
This is not a style preference. A binary project file cannot be reviewed in
|
|
22
|
+
a diff, and it corrupts when two changes land at once — both fatal when
|
|
23
|
+
agents are doing the editing. If the project needs a new target or setting,
|
|
24
|
+
edit `project.yml` and regenerate.
|
|
25
|
+
|
|
26
|
+
## Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Sources/
|
|
30
|
+
├── App/ # features, screens, navigation
|
|
31
|
+
└── DesignSystem/ # the UI kit. no business logic. ever.
|
|
32
|
+
├── Tokens.swift
|
|
33
|
+
└── Button/
|
|
34
|
+
├── Button.swift
|
|
35
|
+
└── ButtonStyle.swift
|
|
36
|
+
Tests/
|
|
37
|
+
├── AppTests/
|
|
38
|
+
└── DesignSystemTests/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every component gets its own folder under `DesignSystem/`, mirroring the
|
|
42
|
+
web and Expo stacks — one mental model across the repo.
|
|
43
|
+
|
|
44
|
+
`DesignSystem/` contains nothing that is not design. A view may hold logic
|
|
45
|
+
about itself; never about your product.
|
|
46
|
+
|
|
47
|
+
## Order of work
|
|
48
|
+
|
|
49
|
+
Tokens before components. Extract every axis from the design source —
|
|
50
|
+
color, spacing, typography, radii, shadows, motion — into `Tokens.swift`
|
|
51
|
+
and the asset catalog. Semantic colors resolve per theme; views reference
|
|
52
|
+
`Color.surface`, never a raw hex or palette entry.
|
|
53
|
+
|
|
54
|
+
A view with a hardcoded `Color(hex:)` or `padding(13)` is a bug.
|
|
55
|
+
|
|
56
|
+
## No ready-made components
|
|
57
|
+
|
|
58
|
+
The kit is built from SwiftUI primitives. No third-party UI package.
|
|
59
|
+
|
|
60
|
+
SwiftUI is the platform, the way UIKit was — it is the substrate, not a UI
|
|
61
|
+
kit. Wrapping its primitives in our own `DesignSystem` components is the
|
|
62
|
+
job; adopting an outside component library is not.
|
|
63
|
+
|
|
64
|
+
## Swift rules
|
|
65
|
+
|
|
66
|
+
- Value types by default. Reach for a class when identity or reference
|
|
67
|
+
semantics are genuinely required.
|
|
68
|
+
- Strict concurrency is on. Fix the warnings; do not silence them with
|
|
69
|
+
`@unchecked Sendable`.
|
|
70
|
+
- UI work on the main actor, explicitly.
|
|
71
|
+
- No force unwrap and no `try!` outside tests. Model absence honestly.
|
|
72
|
+
- `weak self` in escaping closures that capture self.
|
|
73
|
+
|
|
74
|
+
## Memory — ARC does not collect cycles
|
|
75
|
+
|
|
76
|
+
There is no tracing collector here. A reference cycle is never freed, and
|
|
77
|
+
nothing warns you. So:
|
|
78
|
+
|
|
79
|
+
- A closure stored by an object — a handler, a callback, a Combine sink —
|
|
80
|
+
captures `self` weakly. The rule above, applied wherever the closure
|
|
81
|
+
outlives the call.
|
|
82
|
+
- Delegates and parent references are `weak`. The child never owns the
|
|
83
|
+
parent.
|
|
84
|
+
- A `Task` stored in a property is cancelled in `deinit`. A detached task
|
|
85
|
+
that captures `self` strongly keeps the object alive until the task ends.
|
|
86
|
+
- Combine subscriptions live in a `Set<AnyCancellable>` owned by the
|
|
87
|
+
subscriber, so they end when it does.
|
|
88
|
+
- **Every view model has a `deinit` test**: create it, hold it weakly,
|
|
89
|
+
release the strong reference, assert nil. It is five lines, and it is the
|
|
90
|
+
only thing that catches a cycle before a user does.
|
|
91
|
+
- Instruments' Leaks and the memory graph debugger before a release — a
|
|
92
|
+
simulator run with the debugger attached, not an audit from reading.
|
|
93
|
+
|
|
94
|
+
## SwiftUI
|
|
95
|
+
|
|
96
|
+
- Small views. A body longer than a screen wants decomposing.
|
|
97
|
+
- State at the lowest level that can own it.
|
|
98
|
+
- `@Observable` over `ObservableObject` on modern deployment targets.
|
|
99
|
+
- Do not fight the framework with `GeometryReader` where a layout modifier
|
|
100
|
+
exists.
|
|
101
|
+
|
|
102
|
+
## Testing
|
|
103
|
+
|
|
104
|
+
Coverage target is effectively total. The deciding rule:
|
|
105
|
+
|
|
106
|
+
> If one of our functions calls into a third-party library, that function
|
|
107
|
+
> has a test. When a dependency updates and behavior changes, the test
|
|
108
|
+
> must fail.
|
|
109
|
+
|
|
110
|
+
- XCTest for logic. Snapshot tests (`swift-snapshot-testing`) for views.
|
|
111
|
+
- Snapshots catch visual drift and nothing else. Behavior needs its own
|
|
112
|
+
tests — a snapshot passes while a button stops responding.
|
|
113
|
+
- Test both light and dark, and the largest Dynamic Type size.
|
|
114
|
+
|
|
115
|
+
## Accessibility
|
|
116
|
+
|
|
117
|
+
Structural. Every interactive view has a label, a trait, and a value where
|
|
118
|
+
one applies. VoiceOver navigation must be sensible in order, not just
|
|
119
|
+
present. Dynamic Type must not break layout. Minimum touch target 44×44pt.
|
|
120
|
+
|
|
121
|
+
## Tooling
|
|
122
|
+
|
|
123
|
+
`swift-format` with the checked-in configuration. Verification runs through
|
|
124
|
+
`xcodebuild`, not npm — `.orchestra/config.yml` reflects that.
|
|
125
|
+
|
|
126
|
+
## Toolchain
|
|
127
|
+
|
|
128
|
+
Xcode and XcodeGen (`brew install xcodegen`). The scaffold builds and tests
|
|
129
|
+
as generated — `./verify.sh` runs build, test on a simulator, and format.
|
|
130
|
+
Verified against a real simulator run.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Scaffold a new design system component.
|
|
2
|
+
|
|
3
|
+
Usage: `/web:component {Name}`
|
|
4
|
+
|
|
5
|
+
1. Load the `design-component` skill and follow it exactly.
|
|
6
|
+
2. Confirm every value the component needs has a token. If one is missing,
|
|
7
|
+
add it to `src/design/tokens.css` first — do not hardcode.
|
|
8
|
+
3. Create `src/design/{Name}/` with the component, types, tests, fixture,
|
|
9
|
+
and `index.ts`.
|
|
10
|
+
4. Export it from `src/design/index.ts`.
|
|
11
|
+
5. Run `npm run typecheck && npm test && npm run lint`.
|
|
12
|
+
6. Report the a11y checklist with each item explicitly checked or flagged.
|