@tyroneross/build-loop 0.30.3 → 0.35.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/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +11 -2
- package/README.md +8 -7
- package/agents/advisor.md +111 -0
- package/agents/alignment-checker.md +11 -0
- package/agents/assessment-orchestrator.md +1 -1
- package/agents/build-orchestrator.md +28 -33
- package/agents/fact-checker.md +1 -1
- package/agents/fix-critique.md +1 -1
- package/agents/independent-auditor.md +1 -1
- package/agents/overfitting-reviewer.md +1 -1
- package/agents/plan-critic.md +8 -6
- package/agents/promotion-reviewer.md +1 -1
- package/agents/retrospective-synthesizer.md +20 -3
- package/agents/scope-auditor.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/self-improvement-architect.md +1 -1
- package/commands/{handoff.md → compose-handoff.md} +4 -4
- package/commands/{optimize.md → optimize-run.md} +1 -1
- package/commands/{agent-rally-point.md → rally-point.md} +3 -3
- package/commands/{research.md → research-run.md} +1 -1
- package/commands/setup-memory.md +32 -0
- package/commands/{plan-verify.md → verify-plan.md} +1 -1
- package/docs/agent-surface-policy.md +38 -20
- package/docs/memory-setup.md +19 -14
- package/hooks/_resolve_python.sh +28 -0
- package/hooks/closeout.sh +58 -0
- package/hooks/git/pre-push +69 -6
- package/hooks/hooks.json +34 -13
- package/hooks/post-push-closeout.sh +73 -0
- package/hooks/session-start-closeout.sh +69 -0
- package/hooks/session-start-memory.sh +27 -0
- package/hooks/session-start-plugin-heal.sh +33 -0
- package/hooks/test_closeout.sh +124 -0
- package/package.json +7 -4
- package/plugin-artifacts/codex/.codex-plugin/plugin.json +41 -0
- package/plugin-artifacts/codex/AGENTS.md +507 -0
- package/plugin-artifacts/codex/BUILD-ARTIFACT.md +5 -0
- package/plugin-artifacts/codex/LICENSE +202 -0
- package/plugin-artifacts/codex/README.md +404 -0
- package/plugin-artifacts/codex/docs/agent-surface-policy.md +63 -0
- package/plugin-artifacts/codex/references/advisor-dispatch-ladder.md +62 -0
- package/plugin-artifacts/codex/references/agent-role-taxonomy.md +135 -0
- package/plugin-artifacts/codex/references/autonomous-and-per-commit-modes.md +161 -0
- package/plugin-artifacts/codex/references/autonomy-config.md +231 -0
- package/plugin-artifacts/codex/references/capability-routing.md +182 -0
- package/plugin-artifacts/codex/references/codex-subagents.md +106 -0
- package/plugin-artifacts/codex/references/coordination-file-template.md +181 -0
- package/plugin-artifacts/codex/references/coordination-rules.md +283 -0
- package/plugin-artifacts/codex/references/dogfood-reload-checkpoint.md +112 -0
- package/plugin-artifacts/codex/references/halt-and-ask-protocol.md +102 -0
- package/plugin-artifacts/codex/references/implementer-envelope-schema.md +302 -0
- package/plugin-artifacts/codex/references/intent-capability-pack.md +257 -0
- package/plugin-artifacts/codex/references/intent-exploration-prompts.md +96 -0
- package/plugin-artifacts/codex/references/leadership.md +72 -0
- package/plugin-artifacts/codex/references/memory-systems.md +238 -0
- package/plugin-artifacts/codex/references/memory.md +299 -0
- package/plugin-artifacts/codex/references/model-tier-mapping.md +257 -0
- package/plugin-artifacts/codex/references/modular-systems-pack.md +96 -0
- package/plugin-artifacts/codex/references/phase-1-assess.md +231 -0
- package/plugin-artifacts/codex/references/phase-2-plan.md +78 -0
- package/plugin-artifacts/codex/references/phase-3-execute.md +49 -0
- package/plugin-artifacts/codex/references/phase-4-review.md +322 -0
- package/plugin-artifacts/codex/references/phase-5-iterate.md +71 -0
- package/plugin-artifacts/codex/references/phase-6-learn.md +56 -0
- package/plugin-artifacts/codex/references/recent-design-structures.md +274 -0
- package/plugin-artifacts/codex/references/research-trigger-policy.md +140 -0
- package/plugin-artifacts/codex/references/runtime-smoke-triggers.md +42 -0
- package/plugin-artifacts/codex/references/self-review.md +234 -0
- package/plugin-artifacts/codex/references/single-writer-commit-protocol.md +90 -0
- package/plugin-artifacts/codex/references/task-capture-policy.md +68 -0
- package/plugin-artifacts/codex/references/ui-io-contract.md +116 -0
- package/plugin-artifacts/codex/references/ui-spotcheck-protocol.md +65 -0
- package/plugin-artifacts/codex/skills/build-loop/SKILL.md +372 -0
- package/plugin-artifacts/codex/skills/build-loop/detect-plugins.mjs +82 -0
- package/plugin-artifacts/codex/skills/build-loop/eval-guide.md +65 -0
- package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +549 -0
- package/plugin-artifacts/codex/skills/build-loop/phases/fact-check.md +42 -0
- package/plugin-artifacts/codex/skills/build-loop/phases/ui-validation.md +267 -0
- package/plugin-artifacts/codex/skills/build-loop/references/advisor-dispatch-ladder.md +62 -0
- package/plugin-artifacts/codex/skills/build-loop/references/apple-native-planning.md +439 -0
- package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md +161 -0
- package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +182 -0
- package/plugin-artifacts/codex/skills/build-loop/references/codex-subagents.md +106 -0
- package/plugin-artifacts/codex/skills/build-loop/references/coordination.md +161 -0
- package/plugin-artifacts/codex/skills/build-loop/references/correction-aware-capture.md +177 -0
- package/plugin-artifacts/codex/skills/build-loop/references/independent-auditor.md +72 -0
- package/plugin-artifacts/codex/skills/build-loop/references/intent-capability-pack.md +257 -0
- package/plugin-artifacts/codex/skills/build-loop/references/intent-exploration-prompts.md +96 -0
- package/plugin-artifacts/codex/skills/build-loop/references/leadership.md +72 -0
- package/plugin-artifacts/codex/skills/build-loop/references/memory.md +299 -0
- package/plugin-artifacts/codex/skills/build-loop/references/modular-systems-pack.md +96 -0
- package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +129 -0
- package/plugin-artifacts/codex/skills/build-loop/references/pay-it-forward-arch.md +98 -0
- package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +231 -0
- package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +78 -0
- package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +49 -0
- package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +322 -0
- package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +71 -0
- package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +56 -0
- package/plugin-artifacts/codex/skills/build-loop/references/recent-design-structures.md +274 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
- package/plugin-artifacts/codex/skills/build-loop/references/self-recursive-dev.md +77 -0
- package/plugin-artifacts/codex/skills/build-loop/references/self-review.md +234 -0
- package/plugin-artifacts/codex/skills/build-loop/references/ui-io-contract.md +116 -0
- package/plugin-artifacts/codex/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
- package/plugin-artifacts/codex/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
- package/plugin-artifacts/codex/skills/build-loop/templates/backlog-item.md +35 -0
- package/plugin-artifacts/codex/skills/build-loop/templates/codex-worker-prompt.md +100 -0
- package/plugin-artifacts/codex/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
- package/plugin-artifacts/codex/skills/build-loop/templates/ux-fix-plan.md +40 -0
- package/scripts/_paths.py +59 -13
- package/scripts/build_codex_plugin_artifact.py +314 -0
- package/scripts/check_cache_sync.py +1 -1
- package/scripts/install_memory.py +11 -3
- package/scripts/prune_plugin_cache.py +225 -11
- package/scripts/sync_navgator_lessons.py +25 -0
- package/scripts/sync_plugin_cache.py +66 -36
- package/skills/agent-rally-point/SKILL.md +1 -1
- package/skills/agent-rally-watcher/SKILL.md +1 -1
- package/skills/build-loop/SKILL.md +11 -155
- package/skills/build-loop/references/advisor-dispatch-ladder.md +62 -0
- package/skills/build-loop/references/autonomous-and-per-commit-modes.md +161 -0
- package/skills/build-loop/references/capability-routing.md +4 -3
- package/skills/build-loop/references/codex-subagents.md +8 -0
- package/skills/build-loop/references/coordination.md +1 -1
- package/skills/build-loop/references/intent-capability-pack.md +88 -1
- package/skills/build-loop/references/memory.md +17 -2
- package/skills/build-loop/references/phase-1-assess.md +13 -0
- package/skills/build-loop/references/phase-2-plan.md +2 -1
- package/skills/build-loop/references/phase-3-execute.md +4 -2
- package/skills/build-loop/references/phase-4-review.md +12 -0
- package/skills/build-loop/references/phase-6-learn.md +1 -1
- package/skills/build-loop/references/self-review.md +4 -2
- package/skills/build-loop/templates/codex-worker-prompt.md +45 -34
- package/skills/handoff/SKILL.md +3 -3
- package/skills/loop-builder/SKILL.md +98 -0
- package/skills/loop-builder/presets/active-project-evidence.yaml +97 -0
- package/skills/loop-builder/presets/generic-artifact-loop.yaml +95 -0
- package/skills/loop-builder/presets/presentation-audit.yaml +96 -0
- package/skills/loop-builder/presets/research-synthesis.yaml +96 -0
- package/skills/loop-builder/presets/source-ingestion-raw-data-audit.yaml +96 -0
- package/skills/loop-builder/references/spec-format.md +80 -0
- package/skills/loop-builder/scripts/loop_builder.py +346 -0
- package/skills/model-tiering/SKILL.md +65 -40
- package/skills/optimize/SKILL.md +2 -2
- package/skills/plan-verify/SKILL.md +1 -1
- package/skills/plugin-builder/references/distribution.md +8 -0
- package/skills/research/SKILL.md +33 -6
- package/skills/runtime-parity-verification/SKILL.md +51 -0
- package/skills/spec-writing/SKILL.md +19 -0
- package/templates/memory/README.md +65 -0
- package/templates/memory/charter.md.template +46 -0
- package/templates/memory/manifest.json +24 -4
- package/codex-skills/debug-loop/SKILL.md +0 -18
- package/codex-skills/handoff/SKILL.md +0 -22
- package/codex-skills/knowledge/SKILL.md +0 -21
- package/codex-skills/optimize/SKILL.md +0 -18
- package/codex-skills/research/SKILL.md +0 -18
- /package/commands/{knowledge-review.md → review-knowledge.md} +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
|
|
3
|
+
# Apple Native Planning Reference
|
|
4
|
+
|
|
5
|
+
Phase 2 (Plan) guidance for native iOS / macOS / watchOS work. Surfaces the upfront decisions that, when skipped, become Phase 5 (Iterate) emergencies.
|
|
6
|
+
|
|
7
|
+
Source: build-loop run on a sample timer app alarm-not-firing fix (2026-04-26). Generalized from the specific failure modes that bit that build.
|
|
8
|
+
|
|
9
|
+
## When this reference fires
|
|
10
|
+
|
|
11
|
+
`state.json.platform == "apple"` and the goal touches any of:
|
|
12
|
+
- A timed notification, alarm, reminder, or scheduled local push
|
|
13
|
+
- Background session continuity (Pomodoro, fasting, sleep, meditation)
|
|
14
|
+
- Multi-target shared engine (iOS + watchOS, iOS + macOS)
|
|
15
|
+
- Audio + haptic completion routing
|
|
16
|
+
- Focus mode, DND, Time Sensitive, or Critical Alerts behavior
|
|
17
|
+
- Live Activity / Dynamic Island
|
|
18
|
+
|
|
19
|
+
If ANY apply, the planner MUST resolve every required decision below before dispatching Execute subagents.
|
|
20
|
+
|
|
21
|
+
## Required upfront decisions
|
|
22
|
+
|
|
23
|
+
### 1. Targets
|
|
24
|
+
|
|
25
|
+
State the exact list. Do not assume.
|
|
26
|
+
|
|
27
|
+
- iOS app
|
|
28
|
+
- watchOS companion (paired? standalone? complication?)
|
|
29
|
+
- macOS Catalyst, native macOS, or no Mac
|
|
30
|
+
- Live Activity / Dynamic Island widget
|
|
31
|
+
- Home Screen widget (iOS), watchOS widget, macOS menu bar item
|
|
32
|
+
|
|
33
|
+
For each target chosen, the planner adds: deployment target, entitlements list, INFOPLIST keys, code-signing path.
|
|
34
|
+
|
|
35
|
+
Common trap: the "shared" Swift file is included in only one target's source list (XcodeGen `project.yml`). Build error surfaces only when that target rebuilds.
|
|
36
|
+
|
|
37
|
+
### 2. Notification strategy
|
|
38
|
+
|
|
39
|
+
Pick exactly one per use case. Mixing without intent is the root cause of "alarm doesn't fire."
|
|
40
|
+
|
|
41
|
+
| Strategy | When | Cost |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Scheduled `UNTimeIntervalNotificationTrigger` armed at session START | Pomodoro, fasting, meditation — known duration | Need persisted `endDate`, idempotent re-arm on lifecycle events |
|
|
44
|
+
| Scheduled, armed at background only | Foreground-only apps where backgrounding implies "user left" | High failure rate in the source timer-app incident |
|
|
45
|
+
| Reactive (`willPresent` while running) | Foreground-only flows; prefers in-app modal over banner | Doesn't survive force-quit |
|
|
46
|
+
| `interruptionLevel = .timeSensitive` | Most timer apps | Free; user opts in via Focus settings |
|
|
47
|
+
| `interruptionLevel = .critical` | Genuine alarms with Apple-approved Critical Alerts entitlement | Must apply to Apple |
|
|
48
|
+
| BGAppRefreshTask / silent push | Server-driven reminders | Out of scope for local timers |
|
|
49
|
+
|
|
50
|
+
The planner names the strategy in the Plan output. "Scheduled at session start with idempotent re-arm on resume + cancel on pause/reset/skip/completion" is the canonical safe answer for Pomodoro-style timers.
|
|
51
|
+
|
|
52
|
+
### 3. Background modes — what NOT to add
|
|
53
|
+
|
|
54
|
+
Do NOT add `UIBackgroundModes: [audio]` to keep a timer running. App Store rejection is near-certain unless the app is a media player, navigation app, or VOIP. Use scheduled notifications instead. State this constraint explicitly in the plan so Execute subagents don't "fix" silence by adding the mode.
|
|
55
|
+
|
|
56
|
+
### 4. Haptic strategy
|
|
57
|
+
|
|
58
|
+
Haptics are platform-split. Each surface gets its own decision.
|
|
59
|
+
|
|
60
|
+
| Surface | API | Plays through |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| iPhone | `CHHapticEngine` (rich patterns), `UINotificationFeedbackGenerator` (simple), `UIImpactFeedbackGenerator` | Taptic Engine, bypasses ringer switch |
|
|
63
|
+
| Apple Watch | `WKInterfaceDevice.play(_:)` | Watch haptic motor |
|
|
64
|
+
| iPad | `UINotificationFeedbackGenerator` only on supported models | Limited |
|
|
65
|
+
| macOS | None (no haptic hardware on Mac) | n/a |
|
|
66
|
+
|
|
67
|
+
CoreHaptics requires a fallback to `UINotificationFeedbackGenerator` for older devices. Audio and haptic paths must be independent — silenced audio should still haptic.
|
|
68
|
+
|
|
69
|
+
### 5. Audio routing
|
|
70
|
+
|
|
71
|
+
For chime / alarm sounds:
|
|
72
|
+
|
|
73
|
+
- `AVAudioSession` category `.playback` with `mixWithOthers` option to bypass the silent switch (still respects ringer volume). Set in app launch BEFORE any AVAudioPlayer creation.
|
|
74
|
+
- Bundled asset in `Bundle.main` with explicit `forResource:withExtension:` lookup.
|
|
75
|
+
- Always provide `UNNotificationSound.default` as fallback when the bundled asset can't resolve. Silent failure at delivery is the worst outcome.
|
|
76
|
+
- Do NOT use `AVAudioSession.Category.ambient` for alarms — it respects the silent switch.
|
|
77
|
+
- For notification sounds: `UNNotificationSound(named:)` looks up files in the app bundle automatically, NOT the Documents directory.
|
|
78
|
+
|
|
79
|
+
### 6. WatchConnectivity bridge
|
|
80
|
+
|
|
81
|
+
If iOS + watchOS, pick exactly one transport per message type, with rationale.
|
|
82
|
+
|
|
83
|
+
| Transport | Use for |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `sendMessage(_:replyHandler:)` | Live, foreground-to-foreground, expects reply |
|
|
86
|
+
| `transferUserInfo` | FIFO queue, delivers when reachable |
|
|
87
|
+
| `updateApplicationContext` | Latest-state-only, replaces prior pending |
|
|
88
|
+
| `transferFile` | Large blobs |
|
|
89
|
+
| `transferCurrentComplicationUserInfo` | Complication-targeted |
|
|
90
|
+
|
|
91
|
+
Pomodoro state usually wants `updateApplicationContext` (latest state wins) for the timer status, plus `sendMessage` for explicit user actions (start/pause/stop).
|
|
92
|
+
|
|
93
|
+
### 7. Persistence for active session recovery
|
|
94
|
+
|
|
95
|
+
When the app is force-killed or crashes mid-session, recovery must work. Pick one:
|
|
96
|
+
|
|
97
|
+
- `UserDefaults` — simple state (mode, startDate, elapsed, intention). Fits Pomodoro.
|
|
98
|
+
- `SwiftData` / Core Data — complex relational state. Overkill for a single active session.
|
|
99
|
+
- File-based JSON in `Documents/` — when state is JSON-shaped and you want an audit trail.
|
|
100
|
+
|
|
101
|
+
The persisted record MUST include a wall-clock start time (not relative seconds). Recovery code computes age via `Date().timeIntervalSince(startDate)`. Add a hard cap (e.g., 90 min) past which the recovery prompt is suppressed.
|
|
102
|
+
|
|
103
|
+
### 8. Authorization timing (notifications)
|
|
104
|
+
|
|
105
|
+
Three points to choose between:
|
|
106
|
+
|
|
107
|
+
| When | Pros | Cons |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| App launch | Simple | Apple HIG discourages; users decline unexplained prompts |
|
|
110
|
+
| First Start tap | Best context: user committed to a session | Need pre-permission UI sheet (HIG: explain value first) |
|
|
111
|
+
| First completion | Worst — first session runs unauthorized and silently fails | Don't do this |
|
|
112
|
+
|
|
113
|
+
Use the first-Start pattern with a pre-permission sheet that explains why you need it.
|
|
114
|
+
|
|
115
|
+
## Test matrix templates
|
|
116
|
+
|
|
117
|
+
### iPhone (15 states)
|
|
118
|
+
|
|
119
|
+
Reuse the matrix from `build-loop:debugging-memory/references/ios-notification-alarm-playbook.md`. Phase 4 (Review) Validate sub-step references this matrix. Mark every row that requires a real device as `⚠️ device-only` in the scorecard; never claim ✅ on a sim-only verification.
|
|
120
|
+
|
|
121
|
+
### Apple Watch (11 states)
|
|
122
|
+
|
|
123
|
+
| # | Scenario | Expected |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| 1 | Foreground tick to zero | Watch haptic + sound (if not silent) |
|
|
126
|
+
| 2 | Wrist down, watch dimmed | Haptic delivers on wake |
|
|
127
|
+
| 3 | Watch app force-quit | Notification still fires (if scheduled) |
|
|
128
|
+
| 4 | Phone unreachable, watch standalone | Local schedule fires |
|
|
129
|
+
| 5 | Phone reachable, watch+phone both foreground | Single chime, no double |
|
|
130
|
+
| 6 | Mode switch via Watch | iPhone reflects state via WatchConnectivity |
|
|
131
|
+
| 7 | Pause via Watch crown | iPhone pauses, notification cancelled |
|
|
132
|
+
| 8 | Workout running concurrently | Watch app coexists; HRV capture ok |
|
|
133
|
+
| 9 | Always-on display | Tick continues, complication updates |
|
|
134
|
+
| 10 | Charging | Foreground completion still fires |
|
|
135
|
+
| 11 | watchOS Focus mode on | Banner suppressed (expected) |
|
|
136
|
+
|
|
137
|
+
## Common Apple pitfalls
|
|
138
|
+
|
|
139
|
+
| Pitfall | Symptom | Fix |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| Time Sensitive vs Critical Alerts | "Alarm suppressed in Focus / DND" | Time Sensitive entitlement + `interruptionLevel = .timeSensitive`. Critical Alerts requires Apple approval |
|
|
142
|
+
| Silent switch silences alarm | "Alarm doesn't sound" on iPhone | `AVAudioSession.Category.playback` with explicit activate |
|
|
143
|
+
| AVAudioSession `.ambient` for alarms | Same as above | Use `.playback` |
|
|
144
|
+
| Single-target Swift file in multi-target project | `cannot find 'Foo' in scope` in watchOS only | Add to project.yml watchOS sources, or use `#if !os(watchOS)` |
|
|
145
|
+
| `UNNotificationSetting.badgeSetting` on watchOS | Compile error | `#if os(watchOS)` guard |
|
|
146
|
+
| `UIImpactFeedbackGenerator` on macOS | Compile error | `#if !os(macOS)` guard |
|
|
147
|
+
| Shared iOS/macOS SwiftUI sheet uses `.confirmationAction` for dismissal | iOS shows a top Done button, but macOS can render it in a bottom action area or detach it from scroll content | Split chrome by platform: keep iOS toolbar actions; on macOS use an in-content top header with reserved space for Done/Close and put scrollable content below it |
|
|
148
|
+
| Notification scheduled with stale `timeInterval` after recovery | Alarm fires immediately or never | Recompute `endDate - now`, re-arm, cancel if `<= 0` |
|
|
149
|
+
| Live Activity stale endDate | Lock-screen countdown jumps | Update with authoritative `endDate = phaseStartDate + totalTime + pauseAccumulatedDuration` |
|
|
150
|
+
| TestFlight build with new entitlement but no Apple-approved Critical Alerts | Crash on launch on TestFlight | Apply for entitlement before shipping |
|
|
151
|
+
|
|
152
|
+
## Plan-output checklist
|
|
153
|
+
|
|
154
|
+
Phase 2 Plan output for an Apple-native goal should include, in order:
|
|
155
|
+
|
|
156
|
+
1. **Targets**: explicit list with deployment targets
|
|
157
|
+
2. **Notification strategy**: which row from §2, with rationale
|
|
158
|
+
3. **Background modes added**: explicit "none" if no audio/voip; otherwise list with App Store risk note
|
|
159
|
+
4. **Authorization moment**: which from §8
|
|
160
|
+
5. **Audio + haptic split**: per-surface API choice
|
|
161
|
+
6. **WatchConnectivity transports** (if applicable): per-message-type
|
|
162
|
+
7. **Persistence layer**: one of §7
|
|
163
|
+
8. **Test matrix subset**: which rows are sim-verified vs device-deferred
|
|
164
|
+
9. **Diagnostic logging**: what subsystem/category, what events
|
|
165
|
+
10. **Anti-pattern guard**: explicit "do NOT add UIBackgroundModes audio" if relevant
|
|
166
|
+
11. **Shared SwiftUI modal chrome**: for iOS + macOS sheets, state which controls stay in iOS toolbars and which macOS controls move into persistent top content headers. Verify Done/Close remains visible when content scrolls.
|
|
167
|
+
|
|
168
|
+
## When to escalate
|
|
169
|
+
|
|
170
|
+
If Plan can't resolve any of §1-§8 from the goal text + existing repo state, escalate to the user before Execute. These are not safe to assume; the wrong choice becomes a Phase 5 firefight.
|
|
171
|
+
|
|
172
|
+
## watchOS modernization checklist
|
|
173
|
+
|
|
174
|
+
Captured from sample timer app build 73 (2026-04-26). Use during Phase 2 Plan when the goal touches a watchOS target — `*.appiconset` decisions, navigation refactors, or Smart Stack work. Each item is a concrete check, not a recommendation.
|
|
175
|
+
|
|
176
|
+
### Navigation
|
|
177
|
+
|
|
178
|
+
1. **Vertical `TabView(.verticalPage)` is the post-watchOS-10 canonical root.** Three peers max; first tab gets the large title. `NavigationStack` only inside a tab that drills down (e.g. a Customize tab opening a detail form). Crown rotates between tabs.
|
|
179
|
+
2. **Anti-pattern: horizontal page-based TabView.** Apple deprecated the visual model. If the existing root uses it, refactor before any other UI work — every other change inherits the old chrome.
|
|
180
|
+
3. **Anti-pattern: modal sheets stacked >1 deep.** Replace with NavigationStack push or vertical-tab swap. A watch sheet stacked on a sheet is unnavigable on a 41–49mm screen.
|
|
181
|
+
4. **Tab content can resize on watchOS 10+.** Useful for a "running session" tab that should expand to full-screen during an active timer — opt in by giving the running view a larger ideal size and letting the tab grow.
|
|
182
|
+
|
|
183
|
+
### Always-On Display (AOD)
|
|
184
|
+
|
|
185
|
+
5. **Read `\.isLuminanceReduced` in every view that renders accent color, filled shapes, or live private data.** Do not branch on `\.scenePhase` — AOD is a luminance state, not a lifecycle state. UI continues updating at ≤1 Hz under dim.
|
|
186
|
+
6. **Three required AOD adaptations:**
|
|
187
|
+
- Accent → `.foregroundStyle(.secondary)` (or `HierarchicalShapeStyle.secondary` when binding to a `ShapeStyle` slot).
|
|
188
|
+
- Filled `Capsule()`/`Circle()`/`RoundedRectangle().fill(...)` → `.stroke(...)` outline.
|
|
189
|
+
- Hide live biometric or private readouts (heart rate, intention text, anything wrist-down strangers shouldn't see).
|
|
190
|
+
7. **Keep the running countdown legible under AOD.** It's the entire reason a user glances. Lower contrast is fine; hiding it is not.
|
|
191
|
+
8. **Type system note:** ternary across heterogeneous shape styles fails — `ShapeStyle` is not a uniform existential. Wrap with `AnyShapeStyle(HierarchicalShapeStyle.secondary)` vs `AnyShapeStyle(Color.accent)` so Swift can pick a common type.
|
|
192
|
+
|
|
193
|
+
### Liquid Glass (watchOS 26+)
|
|
194
|
+
|
|
195
|
+
9. **Use `#available(watchOS 26.0, *)` conditional, never bump deployment target.** Bumping locks out users on watchOS 11–25, which is most of the install base for ~12 months post-release.
|
|
196
|
+
10. **Wrap as a `ViewModifier`** so call sites stay terse:
|
|
197
|
+
```swift
|
|
198
|
+
private struct GlassPrimaryStyle: ViewModifier {
|
|
199
|
+
func body(content: Content) -> some View {
|
|
200
|
+
if #available(watchOS 26.0, *) {
|
|
201
|
+
content.buttonStyle(.glassProminent)
|
|
202
|
+
} else {
|
|
203
|
+
content.buttonStyle(.borderedProminent)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
11. **Only the primary action per screen gets `.glassProminent`.** Secondary actions use `.glass` (or `.bordered` on fallback). Anti-pattern: glass on every button — defeats the focal weight the material is designed to carry.
|
|
209
|
+
12. **Performance:** if frame drops show on Series 6/7-class hardware under glass, fall back to `.background(.ultraThinMaterial)` (available watchOS 9+) which approximates the look without the blur cost.
|
|
210
|
+
|
|
211
|
+
### Smart Stack + complications
|
|
212
|
+
|
|
213
|
+
13. **Compute `var relevance: TimelineEntryRelevance?` on every `TimelineEntry`.** Smart Stack ranks by `score` (Float, 0–100). Idle/stale → 0. Running primary state → 100. Paused → 10. Secondary phase running → 60. Without `relevance`, the system assumes 0 and the complication never surfaces.
|
|
214
|
+
14. **Set `duration:` to the remaining session window** (e.g. `TimeInterval(timeLeft)`). The system ages the entry out automatically when duration elapses; otherwise the stack pins a stale entry.
|
|
215
|
+
15. **Live Activity auto-pickup on watchOS 26+.** If iOS already starts a Live Activity via `Activity.request(...)`, no Watch-side code needed — Smart Stack surfaces it. This is the highest-leverage Watch feature for any session-based app.
|
|
216
|
+
16. **`supportedFamilies` checklist:** `accessoryCircular`, `accessoryCorner`, `accessoryInline`, `accessoryRectangular`. Skipping any reduces the watch faces a user can pin the complication to.
|
|
217
|
+
17. **ClockKit migration check:** `import ClockKit` should appear nowhere. If users had a ClockKit complication pre-watchOS-9, implement `CLKComplicationWidgetMigrator` so they auto-migrate without re-pinning. Fresh apps skip this.
|
|
218
|
+
|
|
219
|
+
### App icon
|
|
220
|
+
|
|
221
|
+
18. **Single-size 1024×1024 PNG, sRGB, no alpha.** Xcode 14+ generates per-device sizes at build. The runtime applies the circular mask — design with the center 50%-radius circle in mind, ship a square master.
|
|
222
|
+
19. **Separate watchOS catalog when the iOS catalog already has watch entries.** Keeps the modern `idiom: watch` 1024 entry isolated from the legacy iOS-bundled watch icon roles, and lets the watch art evolve independently.
|
|
223
|
+
20. **Override `ASSETCATALOG_COMPILER_APPICON_NAME` per target.** When iOS uses `AppIcon` and watchOS uses `AppIcon-Watch`, the catalogs can both ship without name collision. Set the watchOS-target setting in `project.yml` (XcodeGen) or per-target build settings.
|
|
224
|
+
21. **Verify post-build with `xcrun assetutil`:**
|
|
225
|
+
```bash
|
|
226
|
+
xcrun assetutil --info path/to/SampleTimer.app/Assets.car | grep -i icon
|
|
227
|
+
```
|
|
228
|
+
Expect to see the icon name you supplied. If not, the catalog isn't being picked up by the target.
|
|
229
|
+
|
|
230
|
+
### Anti-patterns (Apple-explicit)
|
|
231
|
+
|
|
232
|
+
- ❌ Horizontal page-based TabView (deprecated visual)
|
|
233
|
+
- ❌ Modal sheets stacked >1 deep
|
|
234
|
+
- ❌ Tables with >5 visible rows (use `ScrollView` + `LazyVStack` or paginate)
|
|
235
|
+
- ❌ Two-finger gestures, long-press menus expecting precision
|
|
236
|
+
- ❌ Bright filled shapes during AOD
|
|
237
|
+
- ❌ ClockKit-only complications (won't ship on new face setups; deprecated watchOS 9)
|
|
238
|
+
- ❌ Group borders on watch — the screen edge is the border
|
|
239
|
+
- ❌ Custom haptic loops outside `WKInterfaceDevice.play(_:)` types
|
|
240
|
+
- ❌ Bumping `WATCHOS_DEPLOYMENT_TARGET` to gain Liquid Glass instead of `#available` conditional
|
|
241
|
+
- ❌ Two `AppIcon.appiconset` directories with the same name — Xcode picks one unpredictably; rename one and override `ASSETCATALOG_COMPILER_APPICON_NAME`
|
|
242
|
+
|
|
243
|
+
### Verification matrix (should run before claiming done)
|
|
244
|
+
|
|
245
|
+
| Check | Command | Pass signal |
|
|
246
|
+
|------|---------|-------------|
|
|
247
|
+
| watchOS builds | `xcodebuild -scheme SampleTimer-watchOS build CODE_SIGNING_ALLOWED=NO` | `BUILD SUCCEEDED` |
|
|
248
|
+
| Widget builds | (deps from watchOS scheme) | no widget-specific errors in log |
|
|
249
|
+
| Icon compiled | `xcrun assetutil --info <app>/Assets.car \| grep -i icon` | watch icon name appears |
|
|
250
|
+
| All schemes | iterate iOS/macOS/iOSWidget/watchOS | each `BUILD SUCCEEDED` |
|
|
251
|
+
| Quality | `./quality-check.sh` | pass count unchanged or improved |
|
|
252
|
+
|
|
253
|
+
Real-device-only (mark ⚠️ in scorecard, never ✅):
|
|
254
|
+
- Liquid Glass rendering at runtime
|
|
255
|
+
- AOD visual under wrist-down
|
|
256
|
+
- Smart Stack relevance ranking on the actual stack
|
|
257
|
+
- Live Activity Smart Stack pickup
|
|
258
|
+
- Home-grid icon appearance
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Tab-vs-drill-in path discipline on watchOS
|
|
263
|
+
|
|
264
|
+
When designing watch IA, every action should have exactly one canonical path.
|
|
265
|
+
|
|
266
|
+
### Tab vs navigation push — when to choose which
|
|
267
|
+
|
|
268
|
+
- **Tab** when the destination is a peer top-level surface that users want to reach from any state. Vertical TabView pages on watchOS feel like rooms, not screens.
|
|
269
|
+
- **Navigation push** when the destination is a hierarchical sub-screen of one parent (e.g. editing a single object's properties).
|
|
270
|
+
|
|
271
|
+
### Redundancy elimination rule
|
|
272
|
+
|
|
273
|
+
If a feature is reachable as both a tab AND an in-screen drill-in, delete the drill-in. Two paths to the same place fragment muscle memory and violate Calm Precision path-discipline.
|
|
274
|
+
|
|
275
|
+
Symptom: user discovers feature A via Tab; later discovers same feature A via Push from inside Tab B; now wonders if they're different.
|
|
276
|
+
|
|
277
|
+
### Worked example — sample timer app WatchModePicker consolidation (build 74)
|
|
278
|
+
|
|
279
|
+
Build 73 introduced a vertical TabView with a Customize tab listing all modes. The pre-existing `WatchModePicker` view (reachable from Timer idle via "Change mode" navigation push) became redundant. Consolidation steps:
|
|
280
|
+
|
|
281
|
+
1. Confirm the drill-in's behavior is fully covered by the tab. If not, port the missing behavior first.
|
|
282
|
+
2. Delete the drill-in's source file. Glob-based target sources (xcodegen `path: watchOS`) make this a one-step removal — no manifest edit needed.
|
|
283
|
+
3. Remove the entry-point link from the parent screen. Replace with a small text hint ("swipe up for modes") so the affordance stays discoverable without a competing nav path.
|
|
284
|
+
4. Grep for zero references in `*.swift`, `*.yml`, `*.pbxproj`.
|
|
285
|
+
|
|
286
|
+
### Tap directness on list items
|
|
287
|
+
|
|
288
|
+
A tap on a list item that has only one logical follow-action should auto-flow into that action. Don't force tap → tap-edit-button.
|
|
289
|
+
|
|
290
|
+
- Preset row: single tap = select + return to invoking surface (set tab selection state).
|
|
291
|
+
- Configurable row (e.g. "Custom"): single tap = push the editor directly. Set the selection state in `simultaneousGesture` so when the user finishes editing and pops, the upstream surface already reflects the choice.
|
|
292
|
+
|
|
293
|
+
The intermediate "select, then tap an Edit button to configure" flow is two-tap UX where one will do.
|
|
294
|
+
|
|
295
|
+
## Visual delta audit pattern
|
|
296
|
+
|
|
297
|
+
For native iOS/macOS modernization passes scoped from a prior audit, run a three-pass workflow before opening any file:
|
|
298
|
+
|
|
299
|
+
1. **Source-code extraction** is load-bearing. The audit must cite `file:line` for every delta with a concrete suggested replacement. Vague reports ("inconsistent typography") force re-extraction during execute and double the cost. The audit doc becomes the spec.
|
|
300
|
+
2. **Canonical reference identification**. Name 2–3 modern reference views in the same repo (e.g. `AlertSettingsView`, `SettingsView`, `ProfileSettingsView`) so the audit's "good" target is in-tree, not abstract HIG.
|
|
301
|
+
3. **Structured delta report**. Group by category (color hardcodes / Dynamic Type / spacing / card pattern / interaction / chart hex), count, and rank by user-impact severity.
|
|
302
|
+
|
|
303
|
+
### Native visual evidence
|
|
304
|
+
|
|
305
|
+
Use built-in simulator screenshots and native AX evidence for visual regression after a redesign that changes layout. Those tools do not extract computed SwiftUI font/color/spacing values, so source-code extraction remains load-bearing for metric audits. Use grep + Read for typography, color, spacing, and token claims. IBR native scans are explicit-only auxiliary evidence when the user asks for IBR.
|
|
306
|
+
|
|
307
|
+
### Three-tier ROI ordering
|
|
308
|
+
|
|
309
|
+
Sequence fixes by descending impact:
|
|
310
|
+
|
|
311
|
+
1. **Colors + dark-mode safety** (highest ROI). Replacing `.foregroundColor(.white.opacity(N))` with `.foregroundStyle(.primary/.secondary/.tertiary)` simultaneously fixes dark-mode adaptation, WCAG contrast, and future-proofs against light-mode variants. Small line count, broad effect.
|
|
312
|
+
2. **Typography Dynamic Type**. Replacing `.font(.system(size: N))` with `.font(.caption / .footnote / .subheadline / .body / .headline / .title3)` enables accessibility text-size scaling. Keep hero anchors (28pt+ ultraLight KPIs) fixed and add `// Hero anchor: intentional fixed size` comments so future audits don't re-flag them.
|
|
313
|
+
3. **Tokens + spacing rhythm**. Hex literals → named Theme tokens; off-grid `padding(.vertical, 6/10/14)` → `Spacing.sm/md/lg`. Lowest individual impact, cumulative polish.
|
|
314
|
+
|
|
315
|
+
### WCAG body-text math for `.white.opacity(N)`
|
|
316
|
+
|
|
317
|
+
Against the LiquidGradientBackground core `#3A4878` (focus mode steel-blue) — a representative dark gradient core — alpha-composited approximations:
|
|
318
|
+
|
|
319
|
+
| Foreground | Approx contrast vs `#3A4878` | WCAG 4.5:1 (body) |
|
|
320
|
+
|---|---|---|
|
|
321
|
+
| `.white.opacity(0.9)` | ~10.6:1 | ✅ pass |
|
|
322
|
+
| `.white.opacity(0.7)` | ~5.7:1 | ✅ pass |
|
|
323
|
+
| `.white.opacity(0.5)` | ~3.8:1 | ⚠️ fails |
|
|
324
|
+
| `.white.opacity(0.35)` | ~2.4:1 | ❌ fails |
|
|
325
|
+
| `.white.opacity(0.3)` | ~2.1:1 | ❌ fails |
|
|
326
|
+
|
|
327
|
+
Anything at or below 0.5 on a dark gradient fails AA body. `.foregroundStyle(.secondary)` resolves to a SwiftUI-managed semantic color that is guaranteed ≥4.5:1 against the backing material in both light and dark color schemes — switch to it instead of tuning opacity by eye.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## WatchConnectivity callback wiring — avoiding dead-signal bugs
|
|
332
|
+
|
|
333
|
+
**Pattern.** When watch sends a signal via `WCSession.sendMessage(_:)`, iPhone's delegate `WCSessionDelegate.session(_:didReceiveMessage:)` decodes the payload and exposes a closure-based published callback on the connectivity manager:
|
|
334
|
+
|
|
335
|
+
```swift
|
|
336
|
+
var onBiometricBreakSignalReceived: ((BiometricBreakSignal) -> Void)?
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
A closure variable like this is useless unless something on the iPhone side assigns into it during app launch. The compiler will not catch a missing assignment because optional closures default to `nil` and silently no-op.
|
|
340
|
+
|
|
341
|
+
**Audit checkpoint.** For every published callback variable on a connectivity manager, grep for at least one assignment site somewhere in iOS code:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
grep -rn "onMyCallback = " iOS/ Shared/
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
If grep returns zero hits, the entire feature path is dead — watch detection runs, the message arrives, and nothing happens on iPhone.
|
|
348
|
+
|
|
349
|
+
**Sample timer app example.** `onBiometricBreakSignalReceived` was declared in build 73 but inspection during build 78 found two related symptoms:
|
|
350
|
+
1. Build 73 wired the closure to `TimerEngine.handleBiometricBreakSignal`, which surfaces an in-app `CheckInData` sheet — but only when the iPhone app is foreground AND a flow session is running ≥15 minutes. In every other state (app backgrounded, no active session, app closed), the signal was effectively dropped.
|
|
351
|
+
2. Build 78 added a second path on the same closure: an opt-in time-sensitive `UNUserNotification` so the signal produces user-visible behavior even when the iPhone app is not in front. Default off; toggle in `AlertSettingsView`.
|
|
352
|
+
|
|
353
|
+
**Recommended patterns.**
|
|
354
|
+
- Prefer `NotificationCenter` for fan-out when more than one subscriber may need the signal — avoids closure-stomping where a later assignment overwrites an earlier one.
|
|
355
|
+
- Or use Combine `PassthroughSubject<Signal, Never>` for typed reactive flow with multi-subscriber semantics.
|
|
356
|
+
- Or make callback assignment a constructor parameter (`init(onSignal: @escaping (Signal) -> Void)`) so the compiler enforces wiring at instantiation.
|
|
357
|
+
- For closure-variable APIs that intentionally allow only one subscriber, add a unit test that calls `connectivityManager.simulateMessage(...)` and asserts a side effect — this catches dead-signal regressions before TestFlight.
|
|
358
|
+
|
|
359
|
+
**Coexisting paths.** When extending an existing closure with a second path (e.g. notification + in-app sheet), put both under the same closure body and gate each independently. Don't reassign the closure — the previous path will be silently lost.
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
## Test target wiring on XcodeGen Apple projects
|
|
363
|
+
|
|
364
|
+
A test target that exists in `project.yml` is only invokable by `xcodebuild test` if it is also a member of a scheme's `test.targets` array. Membership in `targets:` alone makes the bundle compile-clean but unreachable from the test action.
|
|
365
|
+
|
|
366
|
+
**Symptom.** `xcodebuild test -scheme SampleTimer-iOS -only-testing:SampleTimer-UnitTests` returns:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
Cannot test target "SampleTimer-UnitTests"... isn't a member of the specified test plan or scheme
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Fix.** In `project.yml`, the scheme's `test.targets` array must include the test bundle by name:
|
|
373
|
+
|
|
374
|
+
```yaml
|
|
375
|
+
schemes:
|
|
376
|
+
SampleTimer-iOS:
|
|
377
|
+
test:
|
|
378
|
+
config: Debug
|
|
379
|
+
targets:
|
|
380
|
+
- SampleTimer-UnitTests
|
|
381
|
+
- SampleTimer-UITests
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Then regenerate (`xcodegen generate --spec project.yml`).
|
|
385
|
+
|
|
386
|
+
**Platform alignment is part of the wiring.** The test target's `platform:` must match the scheme's runnable destinations. A `bundle.unit-test` declared `platform: macOS` cannot be run from an `SampleTimer-iOS` scheme via `-destination 'platform=iOS Simulator'` — xcodebuild surfaces:
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
Cannot test target "SampleTimer-UnitTests" on "iPhone 17 Pro": ... does not support iphonesimulator
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
If the wiring goal is "iOS scheme runs the unit tests on iPhone simulator," the test target itself must be `platform: iOS` (and any source files it pulls from `Shared/` need to compile cleanly for iOS — which is usually free since the iOS app target already compiles them). When you switch a previously-macOS test target to iOS, audit every scheme that referenced it: the macOS scheme's `test.targets` will silently break unless updated to drop the now-iOS test target or replaced with a separate macOS-platform test bundle.
|
|
393
|
+
|
|
394
|
+
**Audit checkpoint.** Before TestFlight, run a quick grep over `project.yml` to catch test targets not wired anywhere:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
# every test target name should appear in at least one scheme's test action
|
|
398
|
+
yq '.targets | to_entries | map(select(.value.type | test("bundle"))) | .[].key' project.yml
|
|
399
|
+
yq '.schemes | to_entries | map(.value.test.targets // []) | flatten' project.yml
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
If a target name appears in the first list but not the second, the test bundle exists but no scheme can run it.
|
|
403
|
+
|
|
404
|
+
**Sample timer app example (build 79).** `SampleTimer-UnitTests` had been a `bundle.unit-test` on `platform: macOS`, wired into the macOS scheme's `test.targets` only. Builds 71-78 shipped without exercising any of the new code paths in CI. Build 79 added five test files covering AlertConfig codable, pomodoro notification identifier generation, Local Network permission classification, keychain-cache stability, and biometric break-signal default-off wiring. The test target was migrated to `platform: iOS`, added to `SampleTimer-iOS.schemes.test.targets`, and removed from the macOS scheme (which can no longer host an iOS-platform bundle). Net: `xcodebuild test -scheme SampleTimer-iOS -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -only-testing:SampleTimer-UnitTests` runs 51 tests in ~7 seconds.
|
|
405
|
+
|
|
406
|
+
**When test isolation must round-trip through `UserDefaults.standard`.** A common pattern in app-level singletons: `init(defaults: UserDefaults = .standard)` reads from injected defaults, but property `didSet` writes target `UserDefaults.standard` unconditionally. Tests that assert "value persists across two store instances" cannot rely on injected defaults for the write path — they have to either snapshot/restore `.standard` in `setUp`/`tearDown`, or refactor production to plumb the same defaults through both read and write. For a test-only access change this is heavier than a simple `private → internal` flip; document the constraint in the test file rather than push a deeper production change.
|
|
407
|
+
|
|
408
|
+
## SourceKit ghost diagnostics on Xcode 26.x
|
|
409
|
+
|
|
410
|
+
Discovered: Example-iOS-App Run A + Run A.1 (2026-05-20). Generalized so every Apple-platform build-loop run starts inoculated.
|
|
411
|
+
|
|
412
|
+
### Pattern
|
|
413
|
+
|
|
414
|
+
On Xcode 26.x XcodeGen projects, editing `.swift` files and running `xcodegen generate` produces false-positive `<new-diagnostics>` SourceKit errors of the form `Cannot find type 'X' in scope` for types defined in sibling files within the same module. The diagnostics arrive AFTER `xcodebuild` ships `** BUILD SUCCEEDED **`. They are stale index output, not real errors. Upstream tracking: [anthropics/claude-code#46651](https://github.com/anthropics/claude-code/issues/46651).
|
|
415
|
+
|
|
416
|
+
### Two distinct causes, same symptom
|
|
417
|
+
|
|
418
|
+
1. **New file not registered in `.pbxproj`** — file exists on disk, `xcodegen generate` not yet run. Both SourceKit and `xcodebuild` fail. Fix by running `xcodegen generate`.
|
|
419
|
+
2. **Xcode 26.x SourceKit cross-file index lag** — `xcodegen generate` has run, `xcodebuild` succeeds, SourceKit's index is 5–30s behind. Fix is wait or ⌘⇧K in Xcode; do not edit code.
|
|
420
|
+
|
|
421
|
+
Cause 1 is a real problem with a real fix. Cause 2 is a harness/IDE bug; touching code makes things worse.
|
|
422
|
+
|
|
423
|
+
### What the orchestrator must do
|
|
424
|
+
|
|
425
|
+
When `state.json.platform == "apple"` and the goal touches existing `.swift` files, the Phase 2 plan output must include this preamble before any Execute dispatch:
|
|
426
|
+
|
|
427
|
+
> SourceKit `<new-diagnostics>` arriving within ~30s of `xcodegen generate` are presumed stale until `xcodebuild` disagrees. Do not panic-edit a file because the diagnostic stream cannot find a type that grep can.
|
|
428
|
+
|
|
429
|
+
The same preamble belongs in subagent prompts that touch Swift files, per `feedback_subagent_skill_reactivity` (knowledge in CLAUDE.md alone doesn't reach subagents).
|
|
430
|
+
|
|
431
|
+
### When to actually act
|
|
432
|
+
|
|
433
|
+
A SourceKit "Cannot find type" diagnostic is real (not a ghost) when ANY apply:
|
|
434
|
+
- `xcodebuild` produces the same error
|
|
435
|
+
- The named type does not exist anywhere in the module (`grep -rn "<type>" --include="*.swift"` returns nothing)
|
|
436
|
+
- The named type is defined in a different module that is not imported
|
|
437
|
+
- The file was just created and `xcodegen generate` has not been run
|
|
438
|
+
|
|
439
|
+
Otherwise it's a ghost and the fix is patience.
|
package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
# Autonomous Mode + Per-Commit Mode (conditional detail)
|
|
4
|
+
|
|
5
|
+
Extracted from `SKILL.md` (WP-A, 2026-06-09): both modes are conditional — their
|
|
6
|
+
tables and contracts are load-bearing only when the mode is active, yet they
|
|
7
|
+
loaded in every session. The SKILL.md body now carries a one-line pointer to
|
|
8
|
+
each; the full detail lives here and loads on demand.
|
|
9
|
+
|
|
10
|
+
## Autonomous Mode (Queue-Drain Loop)
|
|
11
|
+
|
|
12
|
+
Autonomous mode generalizes Phase 5 Iterate into a self-replenishing worker that drains its own `ux-queue/` + `issues/` + `proposals/`, alignment-checks each item against the original intent, executes the aligned subset, and commits in batches until the queue is empty or the wall-clock budget elapses. Default since this mode shipped (`--autonomous=false` opts back to classic one-pass).
|
|
13
|
+
|
|
14
|
+
**End-of-run backlog/issues drain — SHIPPED DEFAULT 2026-06-04**: every run now auto-drains `.build-loop/issues/` then `.build-loop/backlog/` at end-of-thread without asking. Reversible per-repo via `.build-loop/config.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{ "sessionPrefs": { "continueFromQueues": "never" } }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`PRODUCTION`/`DECISION`-classified items still surface (not auto-executed). The continuation runs the same alignment-checker + scope-auditor + independent-auditor wiring as the in-run iterate loop. Stop conditions: iterate-cap (25 autonomous / 5 classic), budget exhausted, PRODUCTION encountered, 5 consecutive iterate failures, explicit user pause. Surfaced in the run report's `## Queue continuation` section.
|
|
21
|
+
|
|
22
|
+
### Flag surface
|
|
23
|
+
|
|
24
|
+
| Invocation | Effect |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `/build-loop:run "goal text"` | default mode, 2h budget, autonomous=true |
|
|
27
|
+
| `/build-loop:run --long "goal text"` | long mode, 8h budget |
|
|
28
|
+
| `/build-loop:run --budget 4h "goal text"` | custom budget (overrides `--long`) |
|
|
29
|
+
| `/build-loop:run --budget 30m "goal text"` | accepts `30s`, `30m`, `4h`, or bare integer seconds |
|
|
30
|
+
| `/build-loop:run --autonomous=false "goal text"` | classic single-pass; queue items become `followup/` |
|
|
31
|
+
| `/build-loop:run "overnight refactor of auth ..."` | keyword `overnight` → long mode, 8h |
|
|
32
|
+
|
|
33
|
+
**Flag precedence (strict, top wins):**
|
|
34
|
+
|
|
35
|
+
1. `--budget <duration>` — explicit duration always wins; mode tagged `custom`.
|
|
36
|
+
2. `--long` — sets mode `long`, budget 8h.
|
|
37
|
+
3. Keyword detection in goal text — only when `--long` not explicitly set.
|
|
38
|
+
4. Default — mode `default`, budget 2h, autonomous=true.
|
|
39
|
+
|
|
40
|
+
`--autonomous=false` is orthogonal: it can combine with any budget flag but disables the queue-drain loop entirely. With autonomous off, `--budget` still tracks wall-clock but the orchestrator runs classic Phase 1–6 once and reports.
|
|
41
|
+
|
|
42
|
+
### Keyword fallback
|
|
43
|
+
|
|
44
|
+
Case-insensitive whole-word match against the goal text (or `intent.update_intent`). Detection runs ONLY when `--long` is not explicit on the command line. The flag always wins over keyword inference.
|
|
45
|
+
|
|
46
|
+
| Keyword | Example phrasings |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `long` | "long refactor of …" |
|
|
49
|
+
| `long-running` | "long-running migration" |
|
|
50
|
+
| `overnight` | "overnight build" |
|
|
51
|
+
| `large-scale` | "large-scale rewrite" |
|
|
52
|
+
| `multi-day` | "multi-day backfill" |
|
|
53
|
+
|
|
54
|
+
Keyword list is configurable via `.build-loop/config.json.autonomy.keywordsLong[]`. The default list above is hard-coded in the orchestrator.
|
|
55
|
+
|
|
56
|
+
### Budget tracking
|
|
57
|
+
|
|
58
|
+
The orchestrator writes `state.execution.budget` at autonomous-mode start:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mode": "default | long | custom",
|
|
63
|
+
"started_at": "<iso8601 UTC>",
|
|
64
|
+
"deadline_at": "<iso8601 UTC>",
|
|
65
|
+
"last_checkin_at": "<iso8601 UTC> | null",
|
|
66
|
+
"commits_since_push": 0,
|
|
67
|
+
"checkin_interval_pct": 50
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`scripts/budget_check.py` reads this block at every iterate-loop entry, every commit, and every phase boundary, returning a routing envelope (`continue | checkin | finalize_and_stop`). The script is informational — exit 0 always; sub-5ms compute.
|
|
72
|
+
|
|
73
|
+
**Resume contract**: when a budget block exists and the run resumes via `--resume <run_id>`, the orchestrator MUST reuse the original `deadline_at`. A 2h budget that crashed at 1h59m does NOT get a fresh 2h. `scripts/resume_resolver.py._resolve_budget_on_resume()` is the single source of truth for this rule and surfaces the preserved budget under `budget_resume.preserve_deadline: true`.
|
|
74
|
+
|
|
75
|
+
### Iteration caps
|
|
76
|
+
|
|
77
|
+
| Mode | Per-build cap | Per-item cap |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Classic (autonomous=false) | 5 | n/a |
|
|
80
|
+
| Autonomous default | 25 | 3 same-verdict |
|
|
81
|
+
| Autonomous long | 25 | 3 same-verdict |
|
|
82
|
+
|
|
83
|
+
`maxIterateAttemptsAutonomous` is configurable in `.build-loop/config.json.autonomy.maxIterateAttemptsAutonomous`.
|
|
84
|
+
|
|
85
|
+
### Question timeout (autonomous auto-decide)
|
|
86
|
+
|
|
87
|
+
In autonomous / `--long` mode a question that would otherwise block on the human auto-resolves if unanswered within a window, so an unattended run never stalls. When the orchestrator surfaces such a question it states a **recommended default** + a deadline; `scripts/question_timeout.py` is consulted (e.g. on a `ScheduleWakeup` resume) and returns `answered | take_default | wait`. On `take_default` the orchestrator takes the recommended option, records it to `state.execution.autonomousDefaults[]` + `auto-decision-capture`, continues, and lists every auto-decided question in the end-of-run readback for override (prefer the reversible option when deciding).
|
|
88
|
+
|
|
89
|
+
**Never auto-resolves — waits indefinitely:** production push, destructive/irreversible delete, and anything the autonomy gate verdicts `confirm`/`block` (gates #1–#2 in `agents/build-orchestrator.md`). Only reversible / `user_impact: major` decisions (gate #3) and steering clarifications time out — the single production gate is preserved.
|
|
90
|
+
|
|
91
|
+
Config (`.build-loop/config.json.autonomy`): `questionTimeoutMinutes` (default 10), `onTimeout` (`decide_default` default | `wait`).
|
|
92
|
+
|
|
93
|
+
### Per-Phase A constraint
|
|
94
|
+
|
|
95
|
+
Phase A (current ship) wires queue drain + alignment-check + time budget. **Pushes stay manual** — `scripts/autonomous_push.py` and the K-commit batch-push policy ship in Phase B. The `should_push_now` field returned by `budget_check.py` is informational in Phase A; the orchestrator surfaces it in check-ins but does not push autonomously yet.
|
|
96
|
+
|
|
97
|
+
## Per-Commit Mode (Self-Recursive Builds)
|
|
98
|
+
|
|
99
|
+
Per-commit mode splits a multi-commit build into one independent orchestrator dispatch per commit, so each commit reviews and lands cleanly before the next one starts. It activates automatically when the working directory IS the runtime — that is, when the user is editing the build-loop plugin itself (or any plugin whose runtime symlink points back to the working tree). It can also be explicitly opted into or out of via skill arguments.
|
|
100
|
+
|
|
101
|
+
### Detection
|
|
102
|
+
|
|
103
|
+
Phase 1 Assess writes `selfRecursive.enabled: true|false` to `.build-loop/state.json` (commit 1 wired this via `scripts/detect_self_recursive.py`). The skill body MUST read this field BEFORE deciding which dispatch shape to use. If the field is absent, treat it as `false`.
|
|
104
|
+
|
|
105
|
+
### Mode Resolution
|
|
106
|
+
|
|
107
|
+
| Skill arg | `selfRecursive` | Resulting mode |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `--per-commit` (explicit) | either | per-commit |
|
|
110
|
+
| `--no-per-commit` (explicit) | either | single-orchestrator |
|
|
111
|
+
| (none) | true | per-commit (default for self-recursive) |
|
|
112
|
+
| (none) | false | single-orchestrator (today's behavior) |
|
|
113
|
+
|
|
114
|
+
Passing both `--per-commit` and `--no-per-commit` is a user error — fail loud with a one-line message naming the conflict and stop before any dispatch.
|
|
115
|
+
|
|
116
|
+
### Dispatch Contract (Per-Commit Mode)
|
|
117
|
+
|
|
118
|
+
1. **Plan first, dispatch many.** The skill body invokes a single planning orchestrator (Phase 1 Assess + Phase 2 Plan only). Its return must include a per-commit work list at `.build-loop/per-commit-plan.json` with this exact JSON shape:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"run_id": "run_<UTC>_<hash>",
|
|
123
|
+
"commits": [
|
|
124
|
+
{
|
|
125
|
+
"id": "c1",
|
|
126
|
+
"subject": "feat(scripts): add foo helper",
|
|
127
|
+
"scope": "...",
|
|
128
|
+
"files_planned": ["scripts/foo.py", "tests/test_foo.py"],
|
|
129
|
+
"spec": "verbatim packet for the implementer orchestrator",
|
|
130
|
+
"depends_on": []
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"branch": "feat/...",
|
|
134
|
+
"from_branch": "main"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
2. **Per-commit orchestrator dispatch.** For each commit in the plan (respecting `depends_on`), the skill body dispatches a fresh `Agent(subagent_type="build-loop:build-orchestrator", ...)` carrying ONLY that commit's packet plus a `PER_COMMIT_DISPATCH: { commit_id, run_id, prior_commit_hashes }` prompt prefix. Each dispatched orchestrator runs Phase 3 Execute + Phase 4 Review for ITS commit only, then commits and returns. The dispatched orchestrator's behavior on the prefix is documented in `agents/build-orchestrator.md` §0a.
|
|
139
|
+
|
|
140
|
+
3. **Aggregate.** The skill body collects each orchestrator's return envelope and writes a final report combining all commits' results. On partial failure (commit N fails), do NOT dispatch downstream commits; retain `.build-loop/per-commit-plan.json` so a subsequent `/build-loop:run --resume` invocation can pick up where it stopped. **Parent-dispatch contract (GAP-1):** the dispatcher (this skill body) HAS the Agent tool, so it is the parent that owes the audit. For every returned envelope whose `auditor_status` is `not-run:parent-must-dispatch` or `cross-vendor-deferred`, the dispatcher MUST — before declaring that commit/run review-complete — dispatch `Agent(subagent_type="build-loop:independent-auditor")` on that commit's diff range, append the verdict to `.build-loop/judge-decisions.json`, and re-run `write_run_entry --scope build` so the review-completeness gate passes. A nested per-commit orchestrator cannot audit itself; the audit is the dispatcher's responsibility, not an optional step.
|
|
141
|
+
|
|
142
|
+
**Parent owes Phase 6 Learn + retrospective (E3).** A stop-early dispatch that never reaches Phase 4 Review-G can't run Phase 6 Learn or the post-push retro — so the dispatching parent owes them at close, under the same parent-dispatch contract as GAP-1 (not optional, name the owner at dispatch). Full contract: `agents/build-orchestrator.md` §Phase 4 A (E3 block).
|
|
143
|
+
|
|
144
|
+
### State.json schema
|
|
145
|
+
|
|
146
|
+
The per-commit dispatcher tracks its own progress under a `perCommit` block alongside the existing `execution` block:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"perCommit": {
|
|
151
|
+
"enabled": true,
|
|
152
|
+
"mode_source": "self_recursive_default|explicit_flag|opt_out",
|
|
153
|
+
"plan_path": ".build-loop/per-commit-plan.json",
|
|
154
|
+
"completed": [{"commit_id": "c1", "hash": "abc123", "completed_at": "..."}],
|
|
155
|
+
"in_flight": "c2",
|
|
156
|
+
"queued": ["c3"]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
M2's `execution.iterate_attempt` continues to track per-commit-orchestrator attempt counters (each dispatched orchestrator manages its own iterate counter) — do not duplicate iteration tracking inside `perCommit`.
|