diff --git a/Resources/Info.plist b/Resources/Info.plist
index a247c42..2ea1917 100644
--- a/Resources/Info.plist
+++ b/Resources/Info.plist
@@ -13,9 +13,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.3.0
+ 0.3.1
CFBundleVersion
- 10
+ 11
LSApplicationCategoryType
public.app-category.developer-tools
LSMinimumSystemVersion
diff --git a/Sources/CodexLimits/CodexLimitsApp.swift b/Sources/CodexLimits/CodexLimitsApp.swift
index 3f1ae40..13bfea9 100644
--- a/Sources/CodexLimits/CodexLimitsApp.swift
+++ b/Sources/CodexLimits/CodexLimitsApp.swift
@@ -219,11 +219,13 @@ struct CodexLimitsApp: App {
} ?? "unavailable"
return "\(integrations.menuBarMetric.displayName), \(value), \(freshness)"
case .grokCurrentPeriodUsageRemaining:
- let value = grok.currentSnapshot.map {
- $0.remainingPercent.formatted(.number.precision(.fractionLength(0 ... 2)))
- + " percent remaining, \($0.period.rawValue)"
+ let value = grok.currentSnapshot.flatMap { snapshot in
+ snapshot.remainingPercent.map {
+ $0.formatted(.number.precision(.fractionLength(0 ... 2)))
+ + " percent remaining, \(snapshot.period.rawValue)"
+ }
} ?? "unavailable"
- let freshness = grok.currentSnapshot == nil ? "unavailable" : (grok.isStale ? "stale" : "fresh")
+ let freshness = grok.currentSnapshot?.remainingPercent == nil ? "unavailable" : (grok.isStale ? "stale" : "fresh")
return "\(integrations.menuBarMetric.displayName), \(value), \(freshness)"
}
}
diff --git a/Sources/CodexLimits/GrokBillingClient.swift b/Sources/CodexLimits/GrokBillingClient.swift
index 2888c0d..ff3db2a 100644
--- a/Sources/CodexLimits/GrokBillingClient.swift
+++ b/Sources/CodexLimits/GrokBillingClient.swift
@@ -10,7 +10,7 @@ enum GrokBillingError: Error, LocalizedError, Equatable {
case .notFound: "Grok Build could not be found."
case .authenticationRequired: "Sign in with grok login to read your usage."
case .unsupported: "This version of Grok Build does not support usage reads."
- case .missingAllowance: "Grok has not provided an allowance for this account."
+ case .missingAllowance: "Grok did not provide usable usage data."
case .unknownPeriod: "Grok returned an unsupported usage period."
case .invalidReset: "Grok did not provide a valid reset date."
case .invalidResponse: "Grok returned usage data that could not be read."
@@ -24,7 +24,7 @@ enum GrokBillingError: Error, LocalizedError, Equatable {
struct GrokAllowanceSnapshot: Codable, Equatable, Sendable {
enum Period: String, Codable, Sendable { case weekly, monthly }
- let reportedUsedPercent: Double
+ let reportedUsedPercent: Double?
let period: Period
let resetsAt: Date
let observedAt: Date
@@ -38,7 +38,7 @@ struct GrokAllowanceSnapshot: Codable, Equatable, Sendable {
let startsAt: Date?
init(
- reportedUsedPercent: Double,
+ reportedUsedPercent: Double?,
period: Period,
resetsAt: Date,
observedAt: Date,
@@ -65,12 +65,12 @@ struct GrokAllowanceSnapshot: Codable, Equatable, Sendable {
self.startsAt = startsAt
}
- var remainingPercent: Double {
- 100 - min(100, max(0, reportedUsedPercent))
+ var remainingPercent: Double? {
+ reportedUsedPercent.map { 100 - min(100, max(0, $0)) }
}
var isValid: Bool {
- reportedUsedPercent.isFinite
+ (reportedUsedPercent.map(\.isFinite) ?? (measurementSource == "creditUsagePercent"))
&& Self.isSupportedDate(observedAt)
&& Self.isSupportedDate(resetsAt)
&& resetsAt.timeIntervalSince1970 > 0
@@ -96,17 +96,18 @@ struct GrokAllowanceSnapshot: Codable, Equatable, Sendable {
guard let config = result["config"] as? [String: Any] else {
throw GrokBillingError.missingAllowance
}
- let used: Double
+ let used: Double?
let period: Period
let reset: Date
let start: Date?
let source: String
if config.keys.contains("creditUsagePercent")
|| config.keys.contains("currentPeriod") {
- guard let value = number(config["creditUsagePercent"]) else {
+ // A period-only reply still carries current facts; absence is not a zero reading.
+ used = number(config["creditUsagePercent"])
+ guard !config.keys.contains("creditUsagePercent") || used != nil else {
throw GrokBillingError.missingAllowance
}
- used = value
guard let current = config["currentPeriod"] as? [String: Any] else {
throw GrokBillingError.unknownPeriod
}
@@ -122,8 +123,9 @@ struct GrokAllowanceSnapshot: Codable, Equatable, Sendable {
let value = cents(config["used"]) else {
throw GrokBillingError.missingAllowance
}
- used = value / limit * 100
- guard used.isFinite else { throw GrokBillingError.invalidResponse }
+ let percent = value / limit * 100
+ guard percent.isFinite else { throw GrokBillingError.invalidResponse }
+ used = percent
period = .monthly
(start, reset) = try periodDates(
end: config["billingPeriodEnd"], start: config["billingPeriodStart"]
diff --git a/Sources/CodexLimits/GrokIntegration.swift b/Sources/CodexLimits/GrokIntegration.swift
index fc8916c..8739604 100644
--- a/Sources/CodexLimits/GrokIntegration.swift
+++ b/Sources/CodexLimits/GrokIntegration.swift
@@ -167,12 +167,13 @@ final class GrokIntegrationStore: ObservableObject {
}
var menuBarText: String {
- currentSnapshot.map { "\(Int($0.remainingPercent.rounded()))%" } ?? "—"
+ currentSnapshot?.remainingPercent.map { "\(Int($0.rounded()))%" } ?? "—"
}
var statusText: String {
if isRefreshing { return "Checking" }
if let error { return error.localizedDescription }
+ if let currentSnapshot, currentSnapshot.remainingPercent == nil { return "Usage percentage unavailable" }
if snapshot != nil, currentSnapshot == nil { return "New usage observation needed" }
return snapshot == nil ? "Ready to check" : (isStale ? "Stale" : "Ready")
}
@@ -351,8 +352,8 @@ final class GrokIntegrationStore: ObservableObject {
self.historyLoaded = true
}
} else if self.historyVisible {
- let observation = snapshot.historyObservation
- if observation.isValid, !self.history.contains(observation) {
+ if let observation = snapshot.historyObservation,
+ observation.isValid, !self.history.contains(observation) {
self.history.append(observation)
}
}
diff --git a/Sources/CodexLimits/IntegrationAllowanceChart.swift b/Sources/CodexLimits/IntegrationAllowanceChart.swift
index 4577534..f3d1836 100644
--- a/Sources/CodexLimits/IntegrationAllowanceChart.swift
+++ b/Sources/CodexLimits/IntegrationAllowanceChart.swift
@@ -2,9 +2,12 @@ import ClaudeIntegrationCore
import Foundation
extension GrokAllowanceSnapshot {
- var historyObservation: AllowanceObservation {
- AllowanceObservation(
- metric: period == .weekly ? "grok-weekly" : "grok-monthly",
+ var historyMetric: String { period == .weekly ? "grok-weekly" : "grok-monthly" }
+
+ var historyObservation: AllowanceObservation? {
+ guard let remainingPercent else { return nil }
+ return AllowanceObservation(
+ metric: historyMetric,
observedAt: observedAt,
remainingPercent: remainingPercent,
resetsAt: resetsAt,
diff --git a/Sources/CodexLimits/MenuContentView.swift b/Sources/CodexLimits/MenuContentView.swift
index 269717f..909efc2 100644
--- a/Sources/CodexLimits/MenuContentView.swift
+++ b/Sources/CodexLimits/MenuContentView.swift
@@ -325,7 +325,8 @@ struct MenuContentView: View {
private var grokOverviewPrimary: String {
if let snapshot = grok.currentSnapshot {
- return "Usage remaining · \(Int(snapshot.remainingPercent.rounded()))%"
+ return snapshot.remainingPercent.map { "Usage remaining · \(Int($0.rounded()))%" }
+ ?? "Usage percentage unavailable"
}
if grok.snapshot != nil { return "New usage observation needed" }
return grok.isRefreshing ? "Checking" : "Usage is not available"
@@ -347,17 +348,21 @@ struct MenuContentView: View {
Text("Shared across Grok products.")
.font(.callout).foregroundStyle(.secondary)
}
+ if snapshot.remainingPercent == nil {
+ Text("Grok hasn’t reported a usage percentage for this period.")
+ .font(.callout).foregroundStyle(.secondary)
+ }
}
IntegrationUsageRemainingView(
title: "Usage remaining",
- metric: grok.snapshot?.historyObservation.metric ?? grok.history.last?.metric ?? "grok-weekly",
+ metric: grok.snapshot?.historyMetric ?? grok.history.last?.metric ?? "grok-weekly",
observations: grok.history,
current: grok.snapshot?.historyObservation,
now: grok.displayNow,
isStale: grok.isStale,
defaults: chartDefaults
)
- .id(grok.snapshot?.historyObservation.metric ?? grok.history.last?.metric)
+ .id(grok.snapshot?.historyMetric ?? grok.history.last?.metric)
if let snapshot = grok.snapshot {
if let plan = snapshot.subscriptionTier {
LabeledContent("Plan", value: plan)
@@ -373,7 +378,7 @@ struct MenuContentView: View {
}
Divider()
VStack(alignment: .leading, spacing: 6) {
- Text("Last checked \(snapshot.observedAt.formatted(.relative(presentation: .named)))")
+ Text("Last received \(snapshot.observedAt.formatted(.relative(presentation: .named)))")
if grok.isStale {
Label("Stale", systemImage: "clock.badge.exclamationmark")
}
@@ -2721,7 +2726,8 @@ private struct IntegrationUsageRemainingView: View {
Spacer()
Picker("Range", selection: Binding(get: { store.state.timeRange }, set: store.selectTimeRange)) {
ForEach(AnalyticsTimeRange.allCases.filter { $0.isPreset || store.state.timeRange == .selected }) {
- Text($0.rawValue).tag($0)
+ Text($0 == .currentWindow && (current?.resetsAt ?? .distantPast) <= now
+ ? "Last recorded" : $0.rawValue).tag($0)
}
}
.frame(maxWidth: 200)
diff --git a/Tests/CodexLimitsTests/GrokBillingClientTests.swift b/Tests/CodexLimitsTests/GrokBillingClientTests.swift
index 03fdf2a..b72c0b5 100644
--- a/Tests/CodexLimitsTests/GrokBillingClientTests.swift
+++ b/Tests/CodexLimitsTests/GrokBillingClientTests.swift
@@ -6,6 +6,20 @@ final class GrokBillingClientTests: XCTestCase {
private let observedAt = Date(timeIntervalSince1970: 1_700_000_000)
private let current = #"{"config":{"creditUsagePercent":25.5,"currentPeriod":{"type":"USAGE_PERIOD_TYPE_WEEKLY","start":"2030-01-01T00:00:00+00:00","end":"2030-01-08T00:00:00.123456+00:00"},"prepaidBalance":{},"onDemandUsed":{"val":125},"onDemandCap":{"val":2500},"isUnifiedBillingUser":true},"subscription_tier":"Super\u0007Grok","private":"do-not-retain"}"#
+ func testPeriodWithoutPercentageRetainsFactsWithoutInventingUsage() throws {
+ let partial = try decode(current.replacingOccurrences(of: #""creditUsagePercent":25.5,"#, with: ""))
+ XCTAssertNil(partial.reportedUsedPercent)
+ XCTAssertNil(partial.remainingPercent)
+ XCTAssertNil(partial.historyObservation)
+ XCTAssertTrue(partial.isValid)
+ XCTAssertEqual(partial.period, .weekly)
+ XCTAssertEqual(partial.resetsAt.timeIntervalSince1970, 1_894_060_800.123456, accuracy: 0.001)
+ XCTAssertEqual(partial.subscriptionTier, "SuperGrok")
+ XCTAssertEqual(partial.prepaidBalanceUSD, 0)
+ XCTAssertEqual(try JSONDecoder().decode(GrokAllowanceSnapshot.self, from: JSONEncoder().encode(partial)), partial)
+ XCTAssertEqual(try decode(current.replacingOccurrences(of: "25.5", with: "0")).remainingPercent, 100)
+ }
+
func testCurrentAndLegacyAllowancesKeepOnlyValidatedFacts() throws {
let decoded = try decode(current)
XCTAssertEqual(decoded.remainingPercent, 74.5)
diff --git a/Tests/CodexLimitsTests/GrokIntegrationTests.swift b/Tests/CodexLimitsTests/GrokIntegrationTests.swift
index ae05006..d8d7346 100644
--- a/Tests/CodexLimitsTests/GrokIntegrationTests.swift
+++ b/Tests/CodexLimitsTests/GrokIntegrationTests.swift
@@ -4,18 +4,71 @@ import ClaudeIntegrationCore
@MainActor
final class GrokIntegrationTests: XCTestCase {
+ func testPeriodOnlyRefreshAfterResetSurvivesRelaunchAndRecoversWithoutInventedHistory() async throws {
+ let clock = GrokTestClock()
+ let first = fixture(at: clock.now())
+ let source = GrokFetchProbe(snapshot: first)
+ let cache = temporaryDirectory().appendingPathComponent("snapshot.json")
+ let store = makeStore(clock: clock, source: source, cache: cache)
+ await store.setVisible(true)
+ let originalHistory = store.history
+ clock.advance(3_601)
+ let formatter = ISO8601DateFormatter()
+ let reset = clock.now().addingTimeInterval(7 * 86_400)
+ let reply = """
+ {"config":{"currentPeriod":{"type":"USAGE_PERIOD_TYPE_WEEKLY",\
+ "start":"\(formatter.string(from: clock.now()))","end":"\(formatter.string(from: reset))"},\
+ "prepaidBalance":{}},"subscription_tier":"Example plan"}
+ """
+ let partial = try GrokAllowanceSnapshot.decode(Data(reply.utf8), observedAt: clock.now(), sourceVersion: "1.2.3")
+ await source.setSnapshot(partial)
+ await store.refresh()
+ XCTAssertNil(store.error)
+ XCTAssertEqual(store.currentSnapshot?.resetsAt, reset)
+ XCTAssertEqual(store.snapshot?.observedAt, clock.now())
+ XCTAssertEqual(store.snapshot?.subscriptionTier, "Example plan")
+ XCTAssertEqual(store.statusText, "Usage percentage unavailable")
+ XCTAssertEqual(store.menuBarText, "—")
+ XCTAssertEqual(store.history, originalHistory)
+ await store.setVisible(true, includeHistory: false)
+ XCTAssertNil(store.overview, "An old period must not remain the current thumbnail")
+ await store.setEnabled(false)
+
+ let restored = makeStore(clock: clock, source: source, cache: cache)
+ await restored.setVisible(true)
+ XCTAssertEqual(restored.snapshot, partial)
+ XCTAssertEqual(restored.history, originalHistory)
+ XCTAssertNil(restored.currentSnapshot?.remainingPercent)
+
+ clock.advance(600)
+ let zeroReply = reply.replacingOccurrences(of: #""config":{"#, with: #""config":{"creditUsagePercent":0,"#)
+ let zero = try GrokAllowanceSnapshot.decode(Data(zeroReply.utf8), observedAt: clock.now(), sourceVersion: "1.2.3")
+ await source.setSnapshot(zero)
+ await restored.refresh()
+ XCTAssertNil(restored.error)
+ XCTAssertEqual(restored.menuBarText, "100%")
+ XCTAssertEqual(restored.history, originalHistory + [try XCTUnwrap(zero.historyObservation)])
+ let chart = try XCTUnwrap(IntegrationAllowanceChart(
+ metric: zero.historyMetric, observations: restored.history, current: zero.historyObservation,
+ now: clock.now(), isStale: restored.isStale, safetyBuffer: 3
+ ))
+ XCTAssertTrue(chart.chart.currentProjection.isEmpty, "An estimate cannot cross the reset or missing reading")
+ await restored.deleteData()
+ }
+
func testOverviewReadsCurrentPeriodWithoutRetainingDetailAndFallsBackToLatest() async throws {
let clock = GrokTestClock()
let current = fixture(at: clock.now())
+ let currentObservation = try XCTUnwrap(current.historyObservation)
let source = GrokFetchProbe(snapshot: current)
let cache = temporaryDirectory().appendingPathComponent("snapshot.json")
let historyDirectory = cache.deletingLastPathComponent().appendingPathComponent("History")
let previous = AllowanceObservation(
- metric: current.historyObservation.metric, observedAt: current.observedAt.addingTimeInterval(-600),
+ metric: currentObservation.metric, observedAt: current.observedAt.addingTimeInterval(-600),
remainingPercent: 80, resetsAt: current.resetsAt,
- startsAt: current.historyObservation.startsAt, source: current.measurementSource
+ startsAt: currentObservation.startsAt, source: current.measurementSource
)
- let old = fixture(at: clock.now().addingTimeInterval(-20 * 86_400)).historyObservation
+ let old = try XCTUnwrap(fixture(at: clock.now().addingTimeInterval(-20 * 86_400)).historyObservation)
try AllowanceHistory.append([old, previous], in: historyDirectory)
try JSONEncoder().encode(current).write(to: cache)
let files = try FileManager.default.contentsOfDirectory(at: historyDirectory, includingPropertiesForKeys: nil)
@@ -83,16 +136,16 @@ final class GrokIntegrationTests: XCTestCase {
try JSONEncoder().encode(first).write(to: cache)
let store = makeStore(clock: clock, source: source, cache: cache)
await store.setVisible(true)
- XCTAssertEqual(store.history, [first.historyObservation], "Migrate one real cached observation only")
+ XCTAssertEqual(store.history, [try XCTUnwrap(first.historyObservation)], "Migrate one real cached observation only")
clock.advance(600)
let second = fixture(at: clock.now())
await source.setSnapshot(second)
await store.refresh()
- XCTAssertEqual(store.history, [first.historyObservation, second.historyObservation])
+ XCTAssertEqual(store.history, [first, second].compactMap(\.historyObservation))
await store.setEnabled(false)
let restored = makeStore(clock: clock, source: source, cache: cache)
await restored.setVisible(true)
- XCTAssertEqual(restored.history, [first.historyObservation, second.historyObservation])
+ XCTAssertEqual(restored.history, [first, second].compactMap(\.historyObservation))
await restored.setVisible(false)
XCTAssertTrue(restored.history.isEmpty, "Hidden detail releases resident history")
await restored.setVisible(true)
diff --git a/docs/MEASUREMENT-CONTRACT.md b/docs/MEASUREMENT-CONTRACT.md
index a212606..abd944c 100644
--- a/docs/MEASUREMENT-CONTRACT.md
+++ b/docs/MEASUREMENT-CONTRACT.md
@@ -33,6 +33,7 @@ An Integration may publish Account Allowance only when the source identifies the
- A percentage must be finite and source-valid. Missing, null, malformed, or semantically unknown values are unavailable, never zero.
- Claude Code `used_percentage` is valid only inside `0...100`; convert it to `remaining = 100 - used`. Reject an out-of-range value.
- Grok `creditUsagePercent` is a used percentage. Preserve the finite original as `reportedUsedPercent`, apply the official `0...100` clamp, and convert it to remaining. Record `measurementSource` as `creditUsagePercent` or `legacyCredits`, with the CLI version when available. Use legacy `100 * used.val / monthlyLimit.val` only when both current fields are absent; a zero or missing legacy limit is unavailable. A present empty Cent object means zero, while a missing monetary field remains unavailable. The collector sends `_x.ai/billing` on the ACP wire; the internal handler name omits the underscore.
+- A valid Grok `currentPeriod` without `creditUsagePercent` is a successful partial observation: retain its period, reset, observation time, and available account facts, but leave the percentage unavailable. It replaces the latest snapshot without adding an allowance-history point or forecast. Never substitute zero, a previous period’s percentage, or legacy credits for the omitted value. A present null or malformed percentage remains an invalid response. Historic charts identify their range as the last recorded window when no current allowance observation exists; `Last received` dates the latest accepted response rather than the latest refresh attempt.
- A direct remaining percentage is validated without changing its orientation.
- Every allowance window carries its provider window ID or period type, observed time, reset time, and duration when known.
- Weekly and monthly Grok periods remain distinct. Retain a supplied `currentPeriod.start` or legacy `billingPeriodStart` only after supported finite-date and start-before-reset validation. A missing start remains absent; never invent a monthly start from a fixed duration. An unknown period type is unavailable rather than relabeled as weekly.
diff --git a/docs/adr/0015-ship-claude-as-experimental.md b/docs/adr/0015-ship-claude-as-experimental.md
index f99aeb4..d56ed3a 100644
--- a/docs/adr/0015-ship-claude-as-experimental.md
+++ b/docs/adr/0015-ship-claude-as-experimental.md
@@ -12,7 +12,9 @@ Claude Code remains opt-in and is labelled `Experimental` in Settings and its de
The eligible-account check is recorded as `Waived`, not `Passed`, and no longer blocks the release validator.
-The product owner also explicitly waived the expanded all-enabled idle comparison and eight-hour mixed lifecycle soak for release 0.3.0 on 2026-09-10. Neither test was performed. Their PRD rows are `Waived for 0.3.0`; the validator accepts that exact release exception and rejects it for another version. CI, deterministic lifecycle tests, native QA, universal packaging, and update-signature validation remain required.
+The product owner also explicitly waived the expanded all-enabled idle comparison and eight-hour mixed lifecycle soak for release 0.3.0 on 2026-09-10. Neither test was performed. The validator accepts only an exception for the exact release version. CI, deterministic lifecycle tests, native QA, universal packaging, and update-signature validation remain required.
+
+On 2026-09-16, the product owner explicitly renewed both performance exceptions for release 0.3.1. The current PRD rows are `Waived for 0.3.1`; neither check was run or passed, and this renewal does not cover future versions. The owner also authorized an administrative merge of PR #74 without a second/code-owner approval after its tests and CodeQL pass.
## Consequence
diff --git a/docs/prd/multi-integration-workspace.md b/docs/prd/multi-integration-workspace.md
index cfd388f..c92c4b3 100644
--- a/docs/prd/multi-integration-workspace.md
+++ b/docs/prd/multi-integration-workspace.md
@@ -2,7 +2,7 @@
Status: Accepted for v1 implementation
-Release 0.3.0 is accepted with the explicit owner exceptions recorded in [ADR-0015](../adr/0015-ship-claude-as-experimental.md). Unperformed checks remain marked as waived rather than passed.
+Release 0.3.1 is accepted with the explicit owner exceptions recorded in [ADR-0015](../adr/0015-ship-claude-as-experimental.md). Unperformed checks remain marked as waived rather than passed.
## Destination
@@ -329,16 +329,16 @@ Disabling Codex pauses its account timer, local collection, history exchange, an
## v1 release boundary and gates
-Claude Code is opt-in `Experimental` and Grok is opt-in `Beta`; release 0.3.0 is accepted with the recorded owner exceptions below. OpenCode remains deferred. The maturity label appears in Settings and the Integration detail header, not beside every value. Grok also displays CLI-version provenance because its custom ACP billing extension is not a versioned public billing API.
+Claude Code is opt-in `Experimental` and Grok is opt-in `Beta`; release 0.3.1 is accepted with the recorded owner exceptions below. OpenCode remains deferred. The maturity label appears in Settings and the Integration detail header, not beside every value. Grok also displays CLI-version provenance because its custom ACP billing extension is not a versioned public billing API.
| Release gate | Current evidence | Status |
|---|---|---|
| Grok and OpenCode source decision | Grok 1.0.25 accepts correctly prefixed ACP billing with CLI-owned authentication; OpenCode remains excluded by RSS/write budgets | Passed |
| Bounded Codex history and serialized demand-driven collection | 3,650-day fixture, 32-candidate reconciliation, idle process release, deterministic lifecycle tests | Passed |
| Claude relay, setup, privacy, deletion, executable selection, and boundary behavior | Packaged helper checks and deterministic Release tests | Passed |
-| All-enabled idle comparison | Expanded comparison not run; product owner waived it on 2026-09-10 for release 0.3.0; ADR-0015 | Waived for 0.3.0 |
+| All-enabled idle comparison | Expanded comparison not run; product owner renewed the 0.3.0 exception on 2026-09-16 specifically for release 0.3.1; ADR-0015 | Waived for 0.3.1 |
| Eligible Claude account observation | Product owner waived this check on 2026-09-10 for the experimental Claude Code release; ADR-0015 | Waived |
-| Eight-hour mixed lifecycle soak | Not run; product owner waived it on 2026-09-10 for release 0.3.0; ADR-0015 | Waived for 0.3.0 |
+| Eight-hour mixed lifecycle soak | Not run; product owner renewed the 0.3.0 exception on 2026-09-16 specifically for release 0.3.1; ADR-0015 | Waived for 0.3.1 |
`Scripts/validate-release.sh` requires this document to be exactly `Accepted for v1 implementation`. Each required performance row must be `Passed` or explicitly `Waived for VERSION` for the version being released. A version-specific exception does not satisfy later releases; deterministic tests and short comparisons do not count as evidence for an unperformed gate.
@@ -350,7 +350,7 @@ Implementation progress before release acceptance is:
4. Grok billing transport, validated snapshot model, Settings/workspace/menu integration — implemented on 2026-09-10, including retained Grok/Claude history and burndown charts, with a successful compiled collector read and 644 passing Release tests; signed native chart QA passed; expanded performance gates are waived for 0.3.0;
5. OpenCode remains deferred until a supported lighter source passes its gates.
-The normal acceptance criteria, subject to the explicit 0.3.0 exceptions above, are:
+The normal acceptance criteria, subject to the explicit version-specific exceptions above, are:
- provider spikes pass or narrow the scope explicitly; Grok has a working authenticated ACP source and OpenCode remains narrowed out of v1;
- the Codex-only baseline and expanded all-enabled budgets are reproducible; the 2026-08-22 Codex-plus-Claude comparison does not cover Grok, and the expanded idle comparison and eight-hour soak remain unverified;
diff --git a/docs/releasing.md b/docs/releasing.md
index dc83343..9853dbc 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -14,7 +14,7 @@ Losing the EdDSA private key prevents ad-hoc-signed installations from trusting
## Release flow
1. Ask Codex to prepare a release and provide the stable version number.
-2. Codex confirms that the multi-integration PRD is `Accepted for v1 implementation` and that `All-enabled idle comparison` and `Eight-hour mixed lifecycle soak` are recorded as `Passed` or explicitly `Waived for VERSION` for that release, runs the Release tests and QA, then updates `CFBundleShortVersionString` and increments `CFBundleVersion`. The eligible Claude account observation is waived for the experimental release, and both performance checks are waived specifically for 0.3.0 by [ADR-0015](adr/0015-ship-claude-as-experimental.md). No unperformed check is recorded as passed. The validator checks the status and both required rows; it does not block development while they remain pending. The historical Codex-plus-Claude idle comparison does not cover the restored Grok scope or the new provider-local history readers. QA includes history persistence, old-cache seeding, reset and forecast boundaries, and separate Claude/Grok data deletion.
+2. Codex confirms that the multi-integration PRD is `Accepted for v1 implementation` and that `All-enabled idle comparison` and `Eight-hour mixed lifecycle soak` are recorded as `Passed` or explicitly `Waived for VERSION` for that release, runs the Release tests and QA, then updates `CFBundleShortVersionString` and increments `CFBundleVersion`. The eligible Claude account observation is waived for the experimental release; [ADR-0015](adr/0015-ship-claude-as-experimental.md) records separate owner approvals of both performance exceptions for 0.3.0 and 0.3.1. No unperformed check is recorded as passed. The validator checks the status and both required rows; it does not block development while they remain pending. The historical Codex-plus-Claude idle comparison does not cover the restored Grok scope or the new provider-local history readers. QA includes history persistence, old-cache seeding, reset and forecast boundaries, and separate Claude/Grok data deletion.
3. Run `Scripts/validate-release.sh VERSION` and the `Release` workflow with `dry_run` enabled.
4. Inspect the universal app archive, both universal app/helper executables, signed `appcast.xml`, generated notes, and workflow result.
5. Run the workflow with `dry_run` disabled. It creates a Draft Release only.