package manifest import ( "strings" "time" ) type RunManifestStatus string const ( RunManifestStatusRunning RunManifestStatus = "running" RunManifestStatusSucceeded RunManifestStatus = "succeeded" RunManifestStatusFailed RunManifestStatus = "failed" ) type RunStageAction string const ( RunStageActionRun RunStageAction = "run" RunStageActionSkip RunStageAction = "skip" ) // RunStageRecord tracks lifecycle and provenance for one stage within a single invocation. type RunStageRecord struct { Name string `json:"name"` Action RunStageAction `json:"action"` Status StageStatus `json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` StartedAt *time.Time `json:"started_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"` Outputs []ArtifactRecord `json:"outputs,omitempty"` Logs []string `json:"logs,omitempty"` GeneratedConfigs []string `json:"generated_configs,omitempty"` Error *ErrorRecord `json:"error,omitempty"` Metadata map[string]any `json:"metadata,omitempty"` } // RunManifest is the invocation-scoped execution record under runs/{run_id}/manifest.json. type RunManifest struct { SessionID string `json:"session_id"` Campaign string `json:"campaign,omitempty"` RunID string `json:"run_id"` Force bool `json:"force"` RequestedStages []string `json:"requested_stages,omitempty"` SessionManifestPath string `json:"session_manifest_path,omitempty"` LocalWorkDir string `json:"local_workdir,omitempty"` LocalSpoolDir string `json:"local_spool_dir,omitempty"` S3Bucket string `json:"s3_bucket,omitempty"` S3SessionPrefix string `json:"s3_session_prefix,omitempty"` S3RunPrefix string `json:"s3_run_prefix,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` StartedAt *time.Time `json:"started_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"` Status RunManifestStatus `json:"status"` LastError *ErrorRecord `json:"last_error,omitempty"` Stages map[string]*RunStageRecord `json:"stages"` Metadata map[string]any `json:"metadata,omitempty"` } // NewRun constructs a new run manifest with deterministic timestamps. func NewRun(sessionID, campaign, runID string, force bool, requestedStages []string, now time.Time) *RunManifest { return &RunManifest{ SessionID: strings.TrimSpace(sessionID), Campaign: strings.TrimSpace(campaign), RunID: strings.TrimSpace(runID), Force: force, RequestedStages: append([]string(nil), requestedStages...), CreatedAt: now, UpdatedAt: now, StartedAt: timePtr(now), Status: RunManifestStatusRunning, Stages: map[string]*RunStageRecord{}, } } func (m *RunManifest) SetStageAction(name string, action RunStageAction, at time.Time) { s := m.ensureStage(name, at) s.Action = action s.UpdatedAt = at m.UpdatedAt = at } func (m *RunManifest) MarkStageRunning(name string, at time.Time) { s := m.ensureStage(name, at) s.Status = StatusRunning s.StartedAt = timePtr(at) s.CompletedAt = nil s.Error = nil s.UpdatedAt = at m.UpdatedAt = at } func (m *RunManifest) MarkStageSucceeded(name string, at time.Time, outputs []ArtifactRecord) { s := m.ensureStage(name, at) s.Status = StatusSucceeded s.CompletedAt = timePtr(at) s.Error = nil s.Outputs = append([]ArtifactRecord(nil), outputs...) s.UpdatedAt = at m.UpdatedAt = at } func (m *RunManifest) MarkStageFailed(name string, at time.Time, message string) { s := m.ensureStage(name, at) s.Status = StatusFailed s.CompletedAt = timePtr(at) s.Error = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)} s.UpdatedAt = at m.LastError = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)} m.UpdatedAt = at m.Status = RunManifestStatusFailed m.CompletedAt = timePtr(at) } func (m *RunManifest) MarkStageSkipped(name string, at time.Time, reason string) { s := m.ensureStage(name, at) s.Status = StatusSkipped s.CompletedAt = timePtr(at) s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "skipped", At: timePtr(at)} s.UpdatedAt = at m.UpdatedAt = at } func (m *RunManifest) MarkSucceeded(at time.Time) { m.Status = RunManifestStatusSucceeded m.CompletedAt = timePtr(at) m.UpdatedAt = at } func (m *RunManifest) MarkFailed(at time.Time, message string) { m.Status = RunManifestStatusFailed m.CompletedAt = timePtr(at) m.LastError = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)} m.UpdatedAt = at } func (m *RunManifest) ensureStage(name string, at time.Time) *RunStageRecord { if m.Stages == nil { m.Stages = map[string]*RunStageRecord{} } stageName := strings.TrimSpace(name) s, ok := m.Stages[stageName] if !ok || s == nil { s = &RunStageRecord{ Name: stageName, Action: RunStageActionRun, Status: StatusPending, CreatedAt: at, UpdatedAt: at, } m.Stages[stageName] = s } if s.Name == "" { s.Name = stageName } if s.CreatedAt.IsZero() { s.CreatedAt = at } return s }