Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ The predecessor cost metric to [AI Credits (AIC)](#ai-credits-aic), computed as

### Forecast (`gh aw forecast`)

An experimental CLI command that projects future AI Credits (AIC) consumption using a Monte Carlo simulation. It samples historical workflow runs, applies a Poisson-bootstrap algorithm to model run frequency, and returns P10/P50/P90 percentile estimates over a configurable time horizon. Supports both local (`.github/workflows/`) and remote (`--repo`) discovery modes. Output is available as a console table or machine-readable JSON (`--json`). Useful for capacity planning, budget governance, and detecting cost regressions before they occur. See [Forecast Specification](/gh-aw/specs/forecast-specification/).
A CLI command that projects future AI Credits (AIC) consumption using a statistical simulation. It samples historical workflow runs, applies a Poisson-bootstrap algorithm to model run frequency, and returns P10/P50/P90 percentile estimates over a configurable time horizon. Supports both local (`.github/workflows/`) and remote (`--repo`) discovery modes. Output is available as a console table or machine-readable JSON (`--json`). Forecasts are estimates and may be inaccurate. Useful for capacity planning, budget governance, and detecting cost regressions before they occur. See [Forecast Specification](/gh-aw/specs/forecast-specification/).

### Time Between Turns (TBT)

Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/setup/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -640,9 +640,9 @@ Maps PR check rollups to one of the following normalized states: `success`, `fai

`--head-sha` accepts a pre-resolved commit SHA (e.g. from `gh pr list --json headRefOid`) and skips the REST call that would otherwise fetch it from the PR. Use this flag when the SHA is already available to reduce API consumption.

#### `forecast` `[EXPERIMENTAL]`
#### `forecast`

Forecast AI Credit (AIC) usage for agentic workflows using recent run history and Monte Carlo simulation.
Forecast AI Credit (AIC) usage for agentic workflows using recent run history and statistical simulation. All forecasts are estimates derived from historical samples and may be inaccurate.

```bash wrap
gh aw forecast # Forecast all workflows (monthly)
Expand Down
57 changes: 21 additions & 36 deletions docs/src/content/docs/specs/forecast-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ sidebar:

# Forecast Command Specification

**Version**: 0.1.0
**Status**: Experimental Draft
**Version**: 1.0.0
**Status**: Draft
**Latest Version**: [forecast-specification](/gh-aw/specs/forecast-specification/)
**Editor**: GitHub Agentic Workflows Team

> ⚠️ **Experimental**: This specification describes a feature that is under active development. The command interface, output schema, and algorithmic parameters are subject to change without notice. Do not depend on this interface in production workflows.

> [!IMPORTANT]
> AI Credits (AIC) is the primary cost metric in gh-aw. This draft still references legacy Effective Tokens (ET) field names where command output and schema compatibility require them.
> AI Credits (AIC) is the primary cost metric in gh-aw. This document still references legacy Effective Tokens (ET) field names where command output and schema compatibility require them.

---

Expand All @@ -27,27 +25,7 @@ This specification defines the `gh aw forecast` command for the GitHub Agentic W

## Status of This Document

This section describes the status of this document at the time of publication. This is an **Experimental Draft** specification and may be updated, replaced, or made obsolete by other documents at any time. The feature it describes is experimental and not yet subject to the stability guarantees that apply to other gh-aw commands.

Promotion from **Experimental Draft** to **Draft** requires all of the following criteria:

1. At least **3 successful production uses** of `gh aw forecast` documented in repository issue or PR history.
2. A **stable command/API surface** for one full release cycle with no breaking flag or JSON-schema changes.
3. Sustained conformance evidence that all required tests in §12.1 pass in CI for both local and remote discovery paths.

### Promotion Tracking

The table below tracks documented production uses toward the 3-use requirement (criterion 1 above).
When a use is documented in a GitHub issue or PR, add a row with the run identifier, repository,
date, and outcome. Criteria 2 and 3 are evaluated at promotion-review time and are not tracked here.

| `run_id` | `repo` | `date` | `outcome` |
|---|---|---|---|
| [26836211632](https://github.com/github/gh-aw/actions/runs/26836211632) | `github/gh-aw` | 2026-06-02 | Confirmed — daily SPDD spec planner run; forecast engine executed, episode analysis output captured. Referenced in [daily spec work plan issue #2026-06-02](https://github.com/github/gh-aw/issues). |

Update this table when a production use is confirmed. Each entry MUST reference a GitHub issue or
PR that documents the use. Once three **Confirmed** entries are present and criteria 2–3 are
satisfied, initiate the promotion review by opening a specification-update PR.
This section describes the status of this document at the time of publication. This is a **Draft** specification. The `gh aw forecast` command is a stable, generally available command; forecasts it produces are probabilistic estimates and may be inaccurate.

This document is governed by the GitHub Agentic Workflows project specifications process.

Expand Down Expand Up @@ -914,11 +892,11 @@ visibility and access-governance controls.
- **R-IMPL-031**: JSON serialization of numeric fields MUST NOT produce non-finite values (`NaN`, `+Inf`, `-Inf`). If a computation produces a non-finite value, it MUST be replaced with `0` and a warning MUST be emitted.
- **R-IMPL-032**: Implementations MUST NOT round projected ET values in intermediate computations; rounding for display purposes MUST occur only at serialization time.

### 11.5 Experimental Status Behavior
### 11.5 Accuracy Disclosure Behavior

Because the forecast command is marked **Experimental**:
Because forecasts are probabilistic estimates:

- **R-IMPL-040**: The implementation MUST emit a warning to stderr on every invocation indicating the experimental status of the command unless `--json` is specified (JSON callers are assumed to be automated pipelines that handle warnings separately).
- **R-IMPL-040**: The implementation MUST emit a note to stderr on every non-JSON invocation indicating that all forecasts are estimates derived from historical samples and may be inaccurate (JSON callers are assumed to be automated pipelines that handle notices separately).
- **R-IMPL-041**: The JSON output schema MAY have new fields added in minor versions without notice. Callers MUST treat unknown fields as ignorable.

---
Expand Down Expand Up @@ -981,7 +959,7 @@ and adding new fixtures.
- **T-FC-052**: JSON `as_of` field is a valid RFC 3339 UTC timestamp.
- **T-FC-053**: JSON `workflows` array is sorted by `projected_effective_tokens` descending.
- **T-FC-054**: No stdout output (other than JSON) when `--json` is specified.
- **T-FC-055**: Experimental warning emitted to stderr unless `--json` is specified.
- **T-FC-055**: Accuracy note emitted to stderr unless `--json` is specified.

### 12.2 Compliance Checklist

Expand All @@ -1007,7 +985,7 @@ and adding new fixtures.
| Episode table display logic | T-FC-043–044 | 2 | Required |
| Console output columns | T-FC-050 | 1 | Required |
| JSON schema conformance | T-FC-051–054 | 2 | Required |
| Experimental status warning | T-FC-055 | 1 | Required |
| Accuracy disclosure note | T-FC-055 | 1 | Required |

---

Expand Down Expand Up @@ -1069,11 +1047,11 @@ Sync follow-up tasks:
comment on `poissonNormalApproximationThreshold` in `pkg/cli/forecast_montecarlo.go` directing
maintainers to Appendix B and R-FC-060. Closes
[#31985](https://github.com/github/gh-aw/issues/31985).
- **[Resolved 2026-05-28]** R-IMPL-040 experimental-warning sync: Confirmed that
`pkg/cli/forecast.go` (`RunForecast`) emits the experimental-status warning via
`console.FormatWarningMessage` on every non-JSON invocation. The warning is now correctly
suppressed when `config.JSONOutput` is true (per R-IMPL-040 requirement). Verified by
`TestRunForecast_R_IMPL_040_ExperimentalWarning` in `pkg/cli/forecast_test.go`.
- **[Resolved 2026-07-29]** R-IMPL-040 accuracy-note sync: The forecast command no longer
emits an experimental-status warning. `pkg/cli/forecast_render.go` (`renderForecastTable`)
emits an accuracy note — "All forecasts are estimates derived from historical samples and
may be inaccurate." — via `console.FormatWarningMessage` on every non-JSON invocation. The
note is suppressed for `--json` output (per R-IMPL-040).

---

Expand Down Expand Up @@ -1173,6 +1151,13 @@ Safeguard requirements for this specification are now defined in §10.7.

## 17. Change Log

### Version 1.0.0 (Draft)

- Promoted the `gh aw forecast` command out of experimental status
- Removed the experimental-status stderr warning and `[EXPERIMENTAL]` command labels
- Replaced the experimental warning (R-IMPL-040) with an accuracy-disclosure note: "All forecasts are estimates derived from historical samples and may be inaccurate."
- Centralized console output explanations in the footer and removed user-facing "Monte Carlo" wording

### Version 0.1.0 (Experimental Draft)

- Updated remote discovery requirements with workflow-race mitigation guidance (R-DISC-014)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ All diagnostic output MUST go to `stderr` using `console` formatting helpers. St
| `gh aw experiments` | `NewExperimentsCommand` | Explore ongoing A/B experiments in the repository (hidden) |
| `gh aw experiments list` | `NewExperimentsListSubcommand` | List all experiment workflow branches |
| `gh aw experiments analyze` | `NewExperimentsAnalyzeSubcommand` | Analyze a specific experiment workflow in detail |
| `gh aw forecast` | `NewForecastCommand` | Forecast token usage and costs for agentic workflows (experimental) |
| `gh aw forecast` | `NewForecastCommand` | Forecast token usage and costs for agentic workflows |
| `gh aw trial` | `NewTrialCommand` | Run trial workflow executions |
| `gh aw deploy` | `NewDeployCommand` | Deploy agentic workflows to a target repository using a pull request |
| `gh aw outcomes` | `NewOutcomesCommand` | Check what happened to a workflow run's safe outputs |
Expand Down Expand Up @@ -100,7 +100,7 @@ All diagnostic output MUST go to `stderr` using `console` formatting helpers. St
| `RepeatOptions` | `retry.go` | Options for `ExecuteWithRepeat` polling loop |
| `PollOptions` | `signal_aware_poll.go` | Options for `PollWithSignalHandling` |
| `FixConfig` | `fix_command.go` | Configuration for `RunFix` codemods |
| `ForecastConfig` | `forecast_command.go` | Configuration for `NewForecastCommand` (experimental token usage forecasting) |
| `ForecastConfig` | `forecast_command.go` | Configuration for `NewForecastCommand` (token usage forecasting) |
| `ExperimentsListConfig` | `experiments_command.go` | Configuration for `RunExperimentsList` |
| `ExperimentsAnalyzeConfig` | `experiments_command.go` | Configuration for `RunExperimentsAnalyze` |
| `TrialOptions` | `trial_types.go` | Options for `RunWorkflowTrials` |
Expand Down
7 changes: 0 additions & 7 deletions pkg/cli/forecast.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,6 @@ func RunForecast(config ForecastConfig) error {
ctx = timeoutCtx
}

// Emit experimental warning so users know this command is not yet stable.
// Per R-IMPL-040: the warning MUST NOT be emitted when --json is specified,
// as JSON callers are assumed to be automated pipelines that handle warnings separately.
if !config.JSONOutput {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("forecast is an experimental command and may change without notice"))
}

// Validate period.
periodDays, ok := forecastPeriodDays[config.Period]
if !ok {
Expand Down
8 changes: 5 additions & 3 deletions pkg/cli/forecast_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@ type ForecastConfig struct {
func NewForecastCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "forecast [workflow]...",
Short: "[EXPERIMENTAL] Forecast AI Credit (AIC) usage for agentic workflows",
Long: `[EXPERIMENTAL] Forecast AI Credit (AIC) usage for agentic workflows by sampling
Short: "Forecast AI Credit (AIC) usage for agentic workflows",
Long: `Forecast AI Credit (AIC) usage for agentic workflows by sampling
recent run history and projecting forward on a per-week or per-month basis.

The forecaster downloads a sample of recent completed workflow runs and derives
per-run metrics (AIC, duration, success rate). When runs have been
previously processed by 'gh aw logs', cached token-usage data is used. The
observed run frequency is then projected to the target period using a Monte Carlo
observed run frequency is then projected to the target period using a statistical
simulation that models three sources of uncertainty: run count (Poisson), per-run
AIC usage (bootstrap resampling), and per-run success (Bernoulli).

All forecasts are estimates derived from historical samples and may be inaccurate.

Accounts for:
- A/B experiment variants (results are split per variant when present)
- Observed run frequency from GitHub Actions history
Expand Down
12 changes: 7 additions & 5 deletions pkg/cli/forecast_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ func renderForecastTable(output ForecastResult, config ForecastConfig) error {

fmt.Fprintln(os.Stderr, console.FormatInfoMessage(
fmt.Sprintf("Workflow Forecast — weekly & monthly projections (based on last %d days of history)", config.Days)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(
"Cost/projection figures are AI Credits (AIC) — the gh-aw cost metric."))
fmt.Fprintln(os.Stderr, "")

anyUnreliable := false
Expand Down Expand Up @@ -107,13 +105,17 @@ func renderForecastTable(output ForecastResult, config ForecastConfig) error {
}

fmt.Fprintln(os.Stderr, console.FormatInfoMessage(
fmt.Sprintf("AIC = AI Credits. P50 AIC/Run = per-run median AIC; P95 AIC/Run = 95th-percentile per-run AIC; Weekly/Monthly AIC = projected P50 from %d-trial Monte Carlo simulation.", monteCarloIterations)))
"Cost/projection figures are AI Credits (AIC) — the gh-aw cost metric."))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(
"AIC = AI Credits. P50 AIC/Run = per-run median AIC; P95 AIC/Run = 95th-percentile per-run AIC; Weekly/Monthly AIC = projected P50 usage."))
if anyUnreliable {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(
fmt.Sprintf("* Fewer than %d sampled runs — confidence intervals may be unreliable.", minObservationsForReliableForecast)))
fmt.Sprintf("* Fewer than %d sampled runs — projections may be unreliable.", minObservationsForReliableForecast)))
}
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(
"All forecasts are estimates derived from historical samples and may be inaccurate."))
Comment on lines +115 to +116
Comment on lines +115 to +116
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(
fmt.Sprintf("Run '%s forecast --json' for full Monte Carlo output including P10/P90 confidence intervals.", string(constants.CLIExtensionPrefix))))
fmt.Sprintf("Run '%s forecast --json' for full output including P10/P90 confidence intervals.", string(constants.CLIExtensionPrefix))))
return nil
}

Expand Down
32 changes: 0 additions & 32 deletions pkg/cli/forecast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,38 +64,6 @@ func TestRunForecast_InvalidTimeout(t *testing.T) {
require.Error(t, err, "should error for negative timeout")
}

// TestRunForecast_R_IMPL_040_ExperimentalWarning verifies that the experimental status
// warning is emitted to stderr on every non-JSON invocation (R-IMPL-040), and is suppressed
// when --json is specified.
func TestRunForecast_R_IMPL_040_ExperimentalWarning(t *testing.T) {
captureStderr := func(fn func()) string {
r, w, err := os.Pipe()
require.NoError(t, err)
defer r.Close()
orig := os.Stderr
os.Stderr = w
t.Cleanup(func() { os.Stderr = orig })
fn()
// Close the write end before reading so io.ReadAll sees EOF.
require.NoError(t, w.Close())
out, readErr := io.ReadAll(r)
require.NoError(t, readErr)
return string(out)
}

// Without --json: warning MUST appear on stderr.
withoutJSON := captureStderr(func() {
_ = RunForecast(ForecastConfig{Days: 30, Period: "quarter", SampleSize: 10})
})
assert.Contains(t, withoutJSON, "experimental", "R-IMPL-040: warning must appear when --json is not set")

// With --json: warning MUST NOT appear on stderr.
withJSON := captureStderr(func() {
_ = RunForecast(ForecastConfig{Days: 30, Period: "quarter", SampleSize: 10, JSONOutput: true})
})
assert.NotContains(t, withJSON, "experimental", "R-IMPL-040: warning must be suppressed when --json is set")
}

func TestNewForecastCommand_DaysFlagDocumentsAllowedValues(t *testing.T) {
cmd := NewForecastCommand()
require.NotNil(t, cmd)
Expand Down
8 changes: 4 additions & 4 deletions pkg/workflow/model_env_vars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestModelEnvVarInjectionForAgentJob(t *testing.T) {
engine: "claude",
expectedEnvVar: constants.EnvVarModelAgentClaude,
expectedCommand: "${" + constants.EnvVarModelAgentClaude + ":+ --model",
expectedDefault: constants.CopilotBYOKDefaultModel,
expectedDefault: constants.SonnetDefaultModel,
expectedDefaultOverride: compilerenv.DefaultModelClaude,
},
{
Expand Down Expand Up @@ -109,7 +109,7 @@ func TestModelEnvVarInjectionForDetectionJob(t *testing.T) {
name: "Claude detection uses GH_AW_MODEL_DETECTION_CLAUDE",
engine: "claude",
expectedEnvVar: constants.EnvVarModelDetectionClaude,
expectedDefault: constants.CopilotBYOKDefaultModel,
expectedDefault: constants.SonnetDefaultModel,
expectedDefaultOverride: compilerenv.DefaultModelClaude,
},
{
Expand Down Expand Up @@ -495,7 +495,7 @@ func TestExpressionModelUsesEnvVar(t *testing.T) {
model: "${{ inputs.model }}",
expectedModelEnvVar: constants.ClaudeCLIModelEnvVar,
expectedModelEnvVal: "${{ inputs.model }}",
expectedFallbackVal: "${{ vars." + constants.EnvVarModelAgentClaude + " || vars." + compilerenv.DefaultModelClaude + " || '" + constants.CopilotBYOKDefaultModel + "' }}",
expectedFallbackVal: "${{ vars." + constants.EnvVarModelAgentClaude + " || vars." + compilerenv.DefaultModelClaude + " || '" + constants.SonnetDefaultModel + "' }}",
expectShellExpansion: false, // Claude reads ANTHROPIC_MODEL natively, no shell expansion needed
},
{
Expand All @@ -504,7 +504,7 @@ func TestExpressionModelUsesEnvVar(t *testing.T) {
model: "${{ inputs.provider }}/${{ inputs.model }}",
expectedModelEnvVar: constants.ClaudeCLIModelEnvVar,
expectedModelEnvVal: "${{ inputs.provider }}/${{ inputs.model }}",
expectedFallbackVal: "${{ vars." + constants.EnvVarModelAgentClaude + " || vars." + compilerenv.DefaultModelClaude + " || '" + constants.CopilotBYOKDefaultModel + "' }}",
expectedFallbackVal: "${{ vars." + constants.EnvVarModelAgentClaude + " || vars." + compilerenv.DefaultModelClaude + " || '" + constants.SonnetDefaultModel + "' }}",
expectShellExpansion: false,
},
{
Expand Down
Loading
Loading