mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
729fbf8ab6
* feat(rpi): executionPacket criterion contract foundation (UW0 of soc-bcrn) Extend executionPacket with EpicCriteria []rpi.Criterion and BeadCriteria map[string][]rpi.Criterion, backed by a new exported rpi.Criterion type. Add JSON schema (draft 2020-12) covering envelope + criterion shape; if/then enforces agent_judge when check_type == custom_rubric (pre-mortem advisory fix #1). DoneCriteria preserved verbatim for v1 back-compat. New round-trip tests cover criteria-populated, v1-no-criteria, custom-rubric, and omitempty cases. Lands soc-bcrn.1.1 (E1.1). go test ./... → 11911 passed in 50 packages. * feat(skills): acceptance_criteria contract across discovery/plan/crank/validation (UW1 of soc-bcrn) Wire the per-criterion contract through the lifecycle skills (text-only; no code changes). Skill text now references the JSON schema landed in UW0 (schemas/execution-packet.schema.json) as canonical. soc-bcrn.1.2 — discovery/plan emit acceptance_criteria fences: - skills/discovery/SKILL.md STEP 4 + STEP 6 amendments - skills/discovery/references/output-templates.md links the schema - skills/plan/SKILL.md issue-decomposition step requires the block soc-bcrn.1.3 — crank pre-spawn read + per-criterion checkpoint: - Step 4 instructs orchestrator to inject metadata.validation.acceptance_criteria - Step 5.7 checkpoint format adds criterion_verdicts[] soc-bcrn.1.4 (E1.4+E1.5 merged on shared file) — validation: - Per-criterion verdict table replaces vibe-only - Back-compat WARN until 2026-06-30, then FAIL (pre-mortem fix #2) - New skills/validation/references/per-criterion-rubric.md go test ./... → 11833 passed in 50 packages. * fix(skills): correct check_type terminology in crank acceptance-criteria injection Worker B's UW1 amendment hallucinated `kind (test|structure|content|command|lint)` as the criterion field/enum. Actual schema uses `check_type` with enum `test_pass | command_exit_zero | file_exists | grep_match | manual | council_judge | custom_rubric`, plus the conditional `agent_judge` field for `custom_rubric`. Replace with the verbatim shape from schemas/execution-packet.schema.json $defs/Criterion. Caught by self-review of 81d729fc. * feat(rpi,skills): isolation contract + lint + addSignalWithEvidence (UW2 of soc-bcrn) soc-bcrn.2.1 (E2.1+E2.3 merged): - skills/rpi/references/isolation-contract.md (NEW): codifies PRODUCT.md operational principle #5 with declaration-vs-enforcement framing, four-lever table (text / hook / daemon / combination — recommends D), compression patterns + false-positive guard. - scripts/check-skill-isolation.sh (NEW, executable): bash lint that greps phase-skill SKILL.md for compression patterns. Runs clean on current tree; injection-regression test passes (rc=1 on 'I will research' violation). soc-bcrn.3.3 (E3.S2): - session_outcome.go: addSignalWithEvidence(regex, evidence, weight) override path; tests_pass now credited via either regex or evidence. - internal/rpi/artifacts.go: LaneResult + ParseValidationLanesResults (forward-compat — accepts {passed:bool}, {status:"PASS"}, {result:"PASSED"}; empty slice when packet field absent). - rpi_evaluator.go: collectPhaseSessionOutcome reads packet validation_lanes_results and threads any-PASS reduce to evidence path. - session_outcome_test.go: 3 new tests cover override, both-false, regex-alone. UW2 verify-only beads (closed without code): - soc-bcrn.3.1 (E3.S1): captureCreatedEpicID already merged at rpi_phased_discovery.go:43, primary callsite rpi_phased_gates.go:83. - soc-bcrn.3.2 (E3.S3): preserveWorktreeCommits already merged at rpi_cleanup.go:399, called BEFORE git worktree remove --force. go test ./... → 11833 passed in 50 packages. * feat(skills,scripts): isolation-contract cross-links + RPI surface smoke (UW3 of soc-bcrn) soc-bcrn.2.2 (E2.2): Cross-link skills/{rpi,discovery,crank,validation}/SKILL.md to the isolation contract reference doc landed in UW2. Each link uses the lint-allowed `See [...](path)` form. lint runs clean. soc-bcrn.3.4 (E3.S4): scripts/test-rpi-surface-smoke.sh (NEW, executable). Static-surface smoke covering 6 checks: bin presence, schema validity, skill-isolation lint, dangling commits (informational), bd workspace, schema-criterion-fields. Returns PASS/FAIL summary. Path-collision recovery: bead body said scripts/test-evolve-cycle-smoke.sh, but that path was already taken by PR #241 (end-to-end cycle smoke wired into pre-push-gate's --smoke-evolve flag). Restored the original; renamed this new static-surface smoke to scripts/test-rpi-surface-smoke.sh. The two scripts have distinct purposes (cycle E2E vs static surface) and should coexist. go test ./... → 11832 passed in 50 packages. * docs(rpi): rollback discipline note in phase-data-contracts.md (UW4 of soc-bcrn) soc-bcrn.3.5 (E3.W3): document phase-3-first / phase-2-second rollback discipline. Re-routed from the plan's `program-templates/phase-{2,3}.md` path (which doesn't exist in the repo) to the existing phase-data-contracts.md ref doc. Pre-mortem F3 of soc-bcrn flagged this as the primary rollback risk for E3 (consolidated daemon epic). E3.S3 (already merged) is the worked example: preserveWorktreeCommits (phase-3 logic) shipped BEFORE the phase-2 cleanup-removal. soc-bcrn.2.3 (E2.4 verify): closed without code change — isolation-contract.md already has 3 forward-references to rpi_run.go from UW2. go test ./... → 11834 passed in 50 packages. * feat(daemon): RPIRunExecutor in-process runner replaces CLI-fallback shell-out (UW5/5a of soc-bcrn) soc-bcrn.3.6 (E3.W4 sub-wave 5a, additive): - NEW cli/internal/daemon/rpi_run.go: RPIRunExecutor with function-pointer pattern (RPIRunFunc). Mirrors DreamExecutor structure. Validates full-cycle specs (start_phase=1, max_phase=3) and emits artifacts tagged executor_policy=in-process / backend=in-process. - NEW cli/internal/daemon/rpi_run_test.go: 5 tests (runs-via-func, rejects-wrong-jobtype, propagates-runner-error, validate-full-run, requires-fields). - agentopsd.go: buildAgentOpsDaemonCLIFallbackRPIExecutor now wires RPIRunExecutor with defaultCLIFallbackRPIRunFunc that calls runPhasedEngine in-process. New CLIFallbackRPIRunFunc option lets tests inject fakes. buildPhasedEngineOptionsFromSpec maps obvious fields (RunID, TestFirst, ExecutionPacketPath→DiscoveryArtifact, PhaseTimeout); other phasedEngineOptions fields use defaults. - agentopsd_test.go: TestAgentOpsDaemonCLIFallbackExecutorPolicyCompletesRPIRunJob rewritten to inject a fake RPIRunFunc instead of writing fake shell scripts. Asserts executor_policy=in-process, backend=in-process, rpi_run_status=completed. ADDITIVE only — RPICLIExecutor and scripts/ao-rpi-autonomous-cycle.sh preserved for nightly-evolution.sh until sub-waves 5b/5c migrate them. RPICLIExecutor has zero production callers after this change (only its own test file references the type). go test ./... → 11832 passed in 50 packages. * fix(daemon): RPIRunExecutor wraps supervisor (gates+landing+cycles) (UW5/5a-fix of soc-bcrn) soc-bcrn.3.8 — fix the regression sub-wave 5a introduced. Sub-wave 5a's defaultCLIFallbackRPIRunFunc called runPhasedEngine alone, silently losing the gate/landing/cycle semantics of the old shell wrapper that ran ao rpi loop --supervisor. Daemon-submitted rpi.run jobs would have skipped landing-policy enforcement. Changes: - RPIRunJobSpec gains 7 supervisor policy fields (all omitempty for back-compat): MaxCycles, GatePolicy, LandingPolicy, LandingBranch, BDSyncPolicy, FailurePolicy, KillSwitchPath. - agentopsd.go:defaultCLIFallbackRPIRunFunc now calls executeLoopCyclesCtx (thin context-aware wrapper around executeLoopCycles) — full supervisor loop with gates + landing + cycle management. - New buildSupervisorConfigFromSpec(spec, root) helper translates spec policy fields into rpiLoopSupervisorConfig, then validates via the supervisor's own validateLoopConfigPolicies. - Lease disabled in daemon path (cfg.LeaseEnabled=false) — daemon already owns queue-level claim; lease would deadlock. - Deleted dead helper buildPhasedEngineOptionsFromSpec. - 5 new tests: spec round-trip with policy fields, defaults-empty back-compat, spec→cfg mapping (3 cases incl. invalid-enum rejection). Forward-compat caveats noted in worker discoveries (NOT fully wired): - MaxCycles in spec is read but rpiLoopSupervisorConfig has no MaxCycles field; cycle count comes from the cobra-bound rpiMaxCycles global. Daemon path runs one cycle (matches nightly script default). - RunID, ExecutionPacketPath, TestFirst, PhaseTimeout in spec are not yet projected onto the cfg. go test ./... → 11835 passed in 50 packages. * feat(scripts,docs): nightly-evolution + 3 docs migrate to ao daemon submit (UW5/5b of soc-bcrn) soc-bcrn.3.7 (E3.W4 sub-wave 5b): scripts/nightly-evolution.sh — replace shell wrapper invocation (`scripts/ao-rpi-autonomous-cycle.sh`) with `ao daemon jobs submit --type=rpi.run` + `ao daemon jobs wait`. Liveness-probe via `ao daemon jobs list --json` before submit. Payload now carries supervisor policy fields (max_cycles, gate_policy, landing_policy, optional landing_branch) via the spec extension landed in 5a-fix; daemon executor runs the full supervisor loop (cycles + gates + landing). docs/contracts/agentops-daemon.md, docs/runbooks/daemon-factory-admission.md, docs/contracts/factory-admission.md — replace stale references to `scripts/ao-rpi-autonomous-cycle.sh` with the new in-process path (RPIRunExecutor, cli/internal/daemon/rpi_run.go). scripts/ao-rpi-autonomous-cycle.sh and cli/internal/daemon/rpi_cli_executor.go remain in-tree pending sub-wave 5c (deletion). go test ./... → 11835 passed in 50 packages. bash -n scripts/nightly-evolution.sh → clean. * chore(daemon): delete RPICLIExecutor + ao-rpi-autonomous-cycle.sh shell wrapper (UW5/5c of soc-bcrn) soc-bcrn.3 final E3.W4 cleanup. After sub-waves 5a (RPIRunExecutor), 5a-fix (supervisor wrapping + policy fields), and 5b (nightly migration to ao daemon submit), all production callers of these surfaces have been retired. Deleting: - cli/internal/daemon/rpi_cli_executor.go (5.6KB) — old shell-shelling executor - cli/internal/daemon/rpi_cli_executor_test.go (6.6KB) - scripts/ao-rpi-autonomous-cycle.sh (4.7KB) — legacy shell wrapper Final consumer audit: zero non-comment references to RPICLIExecutor or NewRPICLIExecutor remain. The new in-process path (cli/internal/daemon/rpi_run.go → executeLoopCyclesCtx) carries the same gate + landing semantics via supervisor wrapping. go test ./... → 11833 passed in 50 packages (down from 11835 — the 2 diff is from the deleted RPICLIExecutor's own tests). go build ./... + go vet ./... → clean. UW5 (E3.W4 / M3.5 daemon executor swap) complete. * docs(skills): best-practices citation table + 4 SKILL.md cross-links (UW6 of soc-bcrn) soc-bcrn.4 (E4 — Best-practices encoding): E4.1: NEW skills/rpi/references/best-practices.md. Table A (14 lifecycle principles) + Table B (6 anti-patterns) extracted verbatim from .agents/research/2026-05-07-rpi-lifecycle-sharpening.md §Objective 4. Each row carries a source citation and which skills should encode it. The doc explicitly says lifecycle skills MUST cite by # — never duplicate the body. E4.2: skills/{rpi,discovery,crank,validation}/SKILL.md — one-line cross-link added next to the existing isolation-contract.md citation. Each link uses the lint-allowed `See [...](path)` form. lint runs clean. Mechanical-enforcement section in best-practices.md cross-references prior soc-bcrn waves (UW0 schema $defs/Criterion, UW1 per-criterion-rubric, UW2 check-skill-isolation lint). go test ./... → 11832 passed in 50 packages. * feat(daemon,schema): agent-update protocol surface (UW7-1 of soc-y0ct) soc-y0ct.1 — protocol surface (schema + event types + payload structs). Defers ac-aup.3 (rpi_run.go emission) to a follow-up bead since it requires careful integration with the supervisor's existing event flow. - schemas/agent-update.schema.json (NEW): draft 2020-12 envelope {agent_update_version, event_type, payload}; 4 $defs (phase_start, phase_complete, criterion_verdict, phase_handoff) with allOf+if/then for type-tagged payload dispatch (mirrors UW0 Criterion's custom_rubric pattern). - cli/internal/daemon/types.go: 4 new EventType constants registered in the const block AND eventTypeSet (required for ValidateEventType). - cli/internal/daemon/agent_update.go (NEW): AgentUpdateVersion=1, 4 payload structs, 4 NewAgentUpdate*Event constructors that stamp the version, default empty timestamps, emit only-non-zero optional fields. Constructors deliberately leave EventID/RequestID/JobID/Actor unset — emission caller populates those. - cli/internal/daemon/agent_update_test.go (NEW): 7 test functions covering const + ValidateEventType registration, 4 payload round-trips, version stamping, and timestamp default. Status enums per spec: phase_complete uses lowercase (success|failure| timeout) matching JobResultStatus; criterion_verdict uses uppercase (PASS|FAIL|SKIP) matching the verdict vocabulary in schemas/verdict.v1.schema.json. go test ./... → 11842 passed in 50 packages (up from 11833 — the +9 is the new tests under ./internal/daemon/). * feat(tests,docs): wave 0 operator canary + software-factory drift stopgap (soc-kizn.11) Adds tests/e2e/factory-operator-canary.sh: narrow L1/L2 smoke that prints the five operator-surface anchors (briefing, next delivery, validation, evidence, closeout) and reports each as resolved-path or explicit-unavailable. Does not claim factory yield, autonomous merge, or L3 proof. Reframes the Codex section of docs/software-factory.md so the canonical hookless route is `agentopsd` (`ao daemon submit ...`), and `ao codex *` is documented only as deprecated legacy fallback. Strips routine `ao codex *` recommendations from the Surface Map. Validation now passes: `rg 'ao codex (start|stop)|ao codex ensure-(start|stop)' docs/software-factory.md | rg -v 'hookless|compat|legacy|fallback|deprecated'` returns empty. Full Codex skill/runtime parity cleanup remains tracked in soc-kizn.8. * docs(skills): move forge Examples to references/ to fit 248-line CI limit (soc-pul8) skills/forge/SKILL.md was 260 lines after the soc-yjzp.3 cross-vendor analog edit, over the 248-line skill-lint ceiling. Moves the two Examples walkthroughs (SessionEnd hook invocation + manual transcript mining) to skills/forge/references/examples.md, replacing the section body with a link. Now 237 lines. heal.sh --strict clean (new references file is linked). hooks/skill-lint-gate.sh exits 0. Codex parity audit passes. * fix(skills): copy rpi references into crank/discovery/validation, fix dead-ref gate UW3 (e74b2ff0) and UW6 (2272510e) cross-linked from skills/{crank,discovery, validation}/SKILL.md to ../rpi/references/{isolation-contract,best-practices}.md. heal.sh's DEAD_REF check only resolves local references/ and ../shared/references/ paths, so it flagged the cross-skill links as dead — blocking pre-push. Per CLAUDE.md "no symlinks; copy instead", copies both files into each affected skill's own references/ dir and rewrites the SKILL.md links to local paths. skills/validation/SKILL.md additionally compresses Setup+Gate sections, drops the Per-criterion-verdicts example table from Phase Summary code fence, and inlines the back-compat fallback to fit the 248-line skill-lint ceiling (was 274, now 247). Verified: heal.sh --strict clean across all skills; skill-lint-gate exit 0; codex parity audit passes for crank, discovery, validation. * chore(codex,registry): refresh codex artifacts + registry.json after skill ref edits Auto-regenerated by `scripts/refresh-codex-artifacts.sh --scope upstream` after the dead-ref fix touched skills/{crank,discovery,validation}/SKILL.md and skills/forge/SKILL.md (cycle 5 close). Updates .agentops-generated.json hashes for 7 skills + manifest + rolls registry.json reference_count for forge (1 -> 2 after examples.md split) and knowledge_stores count refresh. No source-code changes; this is the metadata catch-up the pre-push codex-artifact-metadata gate flagged. * fix(tests): nightly-evolution bats — mock daemon wait/list, assert payload not log soc-bcrn UW5/5b migrated nightly-evolution.sh from shelling ao-rpi-autonomous-cycle.sh to `ao daemon jobs submit` + `wait`. Two bats tests still asserted the old shell-wrapper behavior: - Mock `ao` only handled `daemon jobs submit`. Add handlers for `daemon jobs wait` (returns completed) and `daemon jobs list` (liveness probe — returns []). Also strip `--output FOO` global flags so subcommand match still works. - Test 462 grep'd $AO_RPI_LOG for `--landing-branch` (the legacy CLI arg). Replace with jq assertion on the new payload file evolve-rpi-run-payload.json. Now 31/31 nightly-evolution bats pass (was 29/31). * fix(skills): copy phase-data-contracts.md to fix transitive broken-link gate isolation-contract.md (just copied to crank/discovery/validation in053111a8) references phase-data-contracts.md as a sibling. The doc-release link validator flagged it as broken in the new locations because the file lived only in skills/rpi/references/. Per CLAUDE.md "no symlinks; copy", copies phase-data-contracts.md into each affected skill's references/ and adds the link to each SKILL.md's Reference Documents section so heal.sh's UNLINKED_REF check stays green. Refreshes codex .agentops-generated.json hashes for the 4 touched skills. Verified: link validator 1990/1990 ok, heal.sh --strict clean, codex artifact metadata + parity audits both pass. * chore(registry): refresh registry.json after phase-data-contracts.md copies Auto-regenerated by scripts/generate-registry.sh after commit3ae3ce85copied phase-data-contracts.md into crank/discovery/validation references. Reference counts: crank 24→27, discovery 5→8, validation +3. CI registry-check (--check mode) flagged the drift on PR #255. * chore(registry): regen without polluting from gitignored .agents/ scripts/generate-registry.sh's build_knowledge_stores walks .agents/ which is gitignored. CI clones clean (no .agents/ tree), so its regen output has no knowledge_stores entries; mine had them populated from session state, causing registry-check stale-detection to fail. Regenerate with .agents/ temporarily moved aside so the committed registry.json matches the CI-clone's expected output. Net effect: drop the knowledge_stores section that was a build-environment artifact. Resolves PR #255 registry-check failure.
783 lines
24 KiB
Bash
Executable File
783 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
nightly-evolution.sh
|
|
|
|
Run or preview the private local AgentOps nightly chain.
|
|
|
|
Default mode is a safe dry-run: it records readiness, planned branch/runtimes,
|
|
open PR context, optional Nightly RPI brief output, and digest files without
|
|
mutating source or starting agent work.
|
|
|
|
Options:
|
|
--execute Allow execution of explicitly enabled phases.
|
|
--run-dream In execute mode, submit a daemon dream.run job.
|
|
--run-evolve In execute mode, run the supervised RPI/evolve wrapper.
|
|
--skip-dream-subprocess Do not fall back to legacy ao overnight start if daemon submit fails.
|
|
--skip-brief Skip scripts/nightly-rpi-brief.sh.
|
|
--emit-systemd Write systemd user service/timer templates to the run dir.
|
|
--repo-root <path> Repository root (default: git top-level or cwd).
|
|
--output-dir <path> Output directory (default: .agents/nightly/<date>/<run-id>).
|
|
--date <YYYY-MM-DD> Nightly date used for branch naming (default: UTC today).
|
|
--runners <csv> Dream runners (default: claude,codex).
|
|
--runtime-cmd <cmd> RPI runtime command for evolve (default: claude).
|
|
--runtime-mode <mode> RPI runtime mode auto|direct|stream|tmux|gc (default: auto).
|
|
--max-cycles <n> Max evolve cycles when --run-evolve is used (default: 1).
|
|
--gate-policy <policy> Evolve gate policy (default: required).
|
|
--landing-policy <policy> Evolve landing policy (default: off).
|
|
--work-order <path> Work order JSON for --execute --run-evolve preflight.
|
|
--landing-branch <name> Override the computed nightly branch for evolve landing.
|
|
--schedule <calendar> systemd OnCalendar value (default: *-*-* 12:15:00 UTC).
|
|
--no-require-ai-sane Do not block execute mode when bushido-box ai-sane fails.
|
|
-h, --help Show this help.
|
|
|
|
Examples:
|
|
scripts/nightly-evolution.sh --emit-systemd
|
|
scripts/nightly-evolution.sh --execute --run-dream
|
|
scripts/nightly-evolution.sh --execute --run-evolve --work-order work-order.json
|
|
EOF
|
|
}
|
|
|
|
die() {
|
|
echo "nightly-evolution: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"
|
|
}
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
|
}
|
|
|
|
json_empty_object() {
|
|
printf '{}\n'
|
|
}
|
|
|
|
json_empty_array() {
|
|
printf '[]\n'
|
|
}
|
|
|
|
resolve_repo_root() {
|
|
local root="$1"
|
|
if [[ -n "$root" ]]; then
|
|
cd "$root" && pwd
|
|
return
|
|
fi
|
|
git rev-parse --show-toplevel 2>/dev/null || pwd
|
|
}
|
|
|
|
run_json_capture() {
|
|
local out_file="$1"
|
|
local err_file="$2"
|
|
shift 2
|
|
|
|
if "$@" >"$out_file" 2>"$err_file"; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
split_csv() {
|
|
local value="$1"
|
|
local -n out_ref="$2"
|
|
local item
|
|
IFS=',' read -r -a out_ref <<<"$value"
|
|
for item in "${out_ref[@]}"; do
|
|
if [[ -z "$item" ]]; then
|
|
die "empty item in comma-separated list: $value"
|
|
fi
|
|
done
|
|
}
|
|
|
|
write_runtime_inventory() {
|
|
local out_tsv="$1"
|
|
shift
|
|
local runtime path
|
|
: >"$out_tsv"
|
|
for runtime in "$@"; do
|
|
path="$(command -v "$runtime" 2>/dev/null || true)"
|
|
if [[ -n "$path" ]]; then
|
|
printf '%s\ttrue\t%s\n' "$runtime" "$path" >>"$out_tsv"
|
|
else
|
|
printf '%s\tfalse\t\n' "$runtime" >>"$out_tsv"
|
|
fi
|
|
done
|
|
}
|
|
|
|
build_blocker_matrix() {
|
|
local prs_json="$1"
|
|
local out_file="$2"
|
|
local gh_evidence="$3"
|
|
local ts
|
|
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
if [[ "$gh_evidence" != "ok" ]]; then
|
|
jq -n --arg status "$gh_evidence" --arg ts "$ts" \
|
|
'{status: $status, checked_at: $ts, prs: []}' >"$out_file"
|
|
return
|
|
fi
|
|
|
|
jq --arg ts "$ts" '
|
|
{
|
|
status: "ok",
|
|
checked_at: $ts,
|
|
prs: [.[] | {
|
|
pr_number: .number,
|
|
head_ref: .headRefName,
|
|
base_ref: .baseRefName,
|
|
changed_file_count: .changedFiles,
|
|
title: .title
|
|
}]
|
|
}' "$prs_json" >"$out_file"
|
|
}
|
|
|
|
build_main_ci_baseline() {
|
|
local out_file="$1"
|
|
local run_dir="$2"
|
|
local gh_evidence="$3"
|
|
local ts
|
|
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
if [[ "$gh_evidence" != "ok" ]]; then
|
|
jq -n --arg status "unknown" --arg ts "$ts" \
|
|
'{status: $status, checked_at: $ts, failed_jobs: []}' >"$out_file"
|
|
return
|
|
fi
|
|
|
|
local ci_json="$run_dir/main-ci-raw.json"
|
|
local ci_err="$run_dir/main-ci-raw.stderr"
|
|
if ! run_json_capture "$ci_json" "$ci_err" \
|
|
gh run list --branch main --limit 1 --json conclusion,databaseId; then
|
|
jq -n --arg status "unknown" --arg ts "$ts" \
|
|
'{status: $status, checked_at: $ts, failed_jobs: []}' >"$out_file"
|
|
return
|
|
fi
|
|
|
|
if [[ "$(jq 'length' "$ci_json")" == "0" ]]; then
|
|
jq -n --arg status "unknown" --arg ts "$ts" \
|
|
'{status: $status, checked_at: $ts, failed_jobs: []}' >"$out_file"
|
|
return
|
|
fi
|
|
|
|
local conclusion run_id ci_status
|
|
conclusion="$(jq -r '.[0].conclusion // "unknown"' "$ci_json")"
|
|
run_id="$(jq -r '.[0].databaseId | tostring' "$ci_json" 2>/dev/null || echo "")"
|
|
ci_status="unknown"
|
|
case "$conclusion" in
|
|
success) ci_status="green" ;;
|
|
failure) ci_status="red" ;;
|
|
esac
|
|
|
|
jq -n --arg status "$ci_status" --arg run_id "$run_id" --arg ts "$ts" \
|
|
'{status: $status, run_id: $run_id, checked_at: $ts, failed_jobs: []}' >"$out_file"
|
|
}
|
|
|
|
preflight_evolve() {
|
|
local wo="$1"
|
|
local repo="$2"
|
|
local runtime_cmd="$3"
|
|
local gh_evidence="$4"
|
|
local main_ci_status="$5"
|
|
local worktree_status="$6"
|
|
local landing_policy="$7"
|
|
local landing_branch="$8"
|
|
|
|
if [[ -z "$wo" || ! -f "$wo" ]]; then
|
|
die "execute+run-evolve requires --work-order; none provided or file not found"
|
|
fi
|
|
|
|
if ! jq -e '.schema_version == 1 and .work_order_id and .target and .allowed_files' "$wo" >/dev/null 2>&1; then
|
|
die "work order is malformed or missing required fields: $wo"
|
|
fi
|
|
|
|
local expires_at now_epoch expires_epoch
|
|
expires_at="$(jq -r '.expires_at // ""' "$wo")"
|
|
if [[ -n "$expires_at" ]]; then
|
|
now_epoch="$(date -u +%s)"
|
|
expires_epoch="$(date -u -d "$expires_at" +%s 2>/dev/null || echo 0)"
|
|
if [[ "$expires_epoch" -gt 0 && "$now_epoch" -gt "$expires_epoch" ]]; then
|
|
die "work order expired at $expires_at"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$worktree_status" ]]; then
|
|
die "execute+run-evolve requires clean worktree; uncommitted changes found"
|
|
fi
|
|
|
|
if [[ "$gh_evidence" != "ok" ]]; then
|
|
die "execute+run-evolve requires GitHub evidence; got: $gh_evidence"
|
|
fi
|
|
|
|
if [[ "$main_ci_status" == "red" ]]; then
|
|
die "execute+run-evolve blocked: main CI is red"
|
|
fi
|
|
|
|
if [[ "$main_ci_status" == "unknown" ]]; then
|
|
die "execute+run-evolve blocked: main CI status is unknown"
|
|
fi
|
|
|
|
if [[ -n "$(git -C "$repo" ls-files .agents 2>/dev/null)" ]]; then
|
|
die "execute+run-evolve blocked: tracked .agents directory found"
|
|
fi
|
|
|
|
if ! command -v "$runtime_cmd" >/dev/null 2>&1; then
|
|
die "execute+run-evolve requires runtime command: $runtime_cmd"
|
|
fi
|
|
|
|
case "$landing_policy" in
|
|
off|manual_pr) ;;
|
|
*) die "execute+run-evolve blocked: landing policy '$landing_policy' not allowed for pilot; use off or manual_pr" ;;
|
|
esac
|
|
|
|
if [[ -n "$landing_branch" ]]; then
|
|
local default_branch
|
|
default_branch="$(git -C "$repo" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || echo main)"
|
|
case "$landing_branch" in
|
|
main|master|"$default_branch")
|
|
die "execute+run-evolve blocked: landing on '$landing_branch' not allowed for pilot; use a nightly/* branch"
|
|
;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
compute_branch() {
|
|
local date_value="$1"
|
|
local base="nightly/${date_value}"
|
|
local candidate="$base"
|
|
local suffix=2
|
|
|
|
if ! git remote get-url origin >/dev/null 2>&1; then
|
|
printf '%s\n' "$candidate"
|
|
return
|
|
fi
|
|
|
|
while git ls-remote --exit-code --heads origin "$candidate" >/dev/null 2>&1; do
|
|
candidate="${base}-v${suffix}"
|
|
suffix=$((suffix + 1))
|
|
done
|
|
printf '%s\n' "$candidate"
|
|
}
|
|
|
|
emit_systemd_templates() {
|
|
local dir="$1"
|
|
local repo_root="$2"
|
|
local schedule="$3"
|
|
local runners="$4"
|
|
local runtime_cmd="$5"
|
|
local max_cycles="$6"
|
|
mkdir -p "$dir"
|
|
|
|
cat >"$dir/agentops-nightly-evolution.service" <<EOF
|
|
[Unit]
|
|
Description=AgentOps private local nightly evolution
|
|
Documentation=file://${repo_root}/docs/runbooks/nightly-evolution.md
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
WorkingDirectory=${repo_root}
|
|
ExecStart=${repo_root}/scripts/nightly-evolution.sh --execute --run-dream --run-evolve --runners ${runners} --runtime-cmd ${runtime_cmd} --max-cycles ${max_cycles}
|
|
EOF
|
|
|
|
cat >"$dir/agentops-nightly-evolution.timer" <<EOF
|
|
[Unit]
|
|
Description=Schedule AgentOps private local nightly evolution
|
|
|
|
[Timer]
|
|
OnCalendar=${schedule}
|
|
Persistent=true
|
|
RandomizedDelaySec=10m
|
|
Unit=agentops-nightly-evolution.service
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
}
|
|
|
|
write_markdown_digest() {
|
|
local path="$1"
|
|
local run_id="$2"
|
|
local mode="$3"
|
|
local branch="$4"
|
|
local ai_sane_status="$5"
|
|
local dream_status="$6"
|
|
local evolve_status="$7"
|
|
local systemd_dir="$8"
|
|
local gh_evidence="${9:-unavailable}"
|
|
local main_ci_status="${10:-unknown}"
|
|
|
|
{
|
|
printf '# Nightly Evolution Digest\n\n'
|
|
printf -- '- Run ID: `%s`\n' "$run_id"
|
|
printf -- '- Mode: `%s`\n' "$mode"
|
|
printf -- '- Planned branch: `%s`\n' "$branch"
|
|
printf -- '- AI readiness: `%s`\n' "$ai_sane_status"
|
|
printf -- '- GitHub evidence: `%s`\n' "$gh_evidence"
|
|
printf -- '- Main CI baseline: `%s`\n' "$main_ci_status"
|
|
printf -- '- Dream phase: `%s`\n' "$dream_status"
|
|
printf -- '- Evolve phase: `%s`\n' "$evolve_status"
|
|
if [[ -n "$systemd_dir" ]]; then
|
|
printf -- '- Scheduler templates: `%s`\n' "$systemd_dir"
|
|
fi
|
|
printf '\n'
|
|
} >"$path"
|
|
}
|
|
|
|
EXECUTE=false
|
|
RUN_DREAM=false
|
|
RUN_EVOLVE=false
|
|
SKIP_DREAM_SUBPROCESS=false
|
|
SKIP_BRIEF=false
|
|
EMIT_SYSTEMD=false
|
|
REQUIRE_AI_SANE=true
|
|
REPO_ROOT=""
|
|
OUTPUT_DIR=""
|
|
RUN_DATE="$(date -u +%F)"
|
|
RUNNERS="claude,codex"
|
|
RUNTIME_CMD="claude"
|
|
RUNTIME_MODE="auto"
|
|
MAX_CYCLES="1"
|
|
GATE_POLICY="required"
|
|
LANDING_POLICY="off"
|
|
WORK_ORDER=""
|
|
LANDING_BRANCH_OVERRIDE=""
|
|
SCHEDULE="*-*-* 12:15:00 UTC"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--execute)
|
|
EXECUTE=true
|
|
shift
|
|
;;
|
|
--run-dream)
|
|
RUN_DREAM=true
|
|
shift
|
|
;;
|
|
--run-evolve)
|
|
RUN_EVOLVE=true
|
|
shift
|
|
;;
|
|
--skip-dream-subprocess)
|
|
SKIP_DREAM_SUBPROCESS=true
|
|
shift
|
|
;;
|
|
--skip-brief)
|
|
SKIP_BRIEF=true
|
|
shift
|
|
;;
|
|
--emit-systemd)
|
|
EMIT_SYSTEMD=true
|
|
shift
|
|
;;
|
|
--repo-root)
|
|
REPO_ROOT="${2:-}"
|
|
shift 2
|
|
;;
|
|
--output-dir)
|
|
OUTPUT_DIR="${2:-}"
|
|
shift 2
|
|
;;
|
|
--date)
|
|
RUN_DATE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--runners)
|
|
RUNNERS="${2:-}"
|
|
shift 2
|
|
;;
|
|
--runtime-cmd)
|
|
RUNTIME_CMD="${2:-}"
|
|
shift 2
|
|
;;
|
|
--runtime-mode)
|
|
RUNTIME_MODE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--max-cycles)
|
|
MAX_CYCLES="${2:-}"
|
|
shift 2
|
|
;;
|
|
--gate-policy)
|
|
GATE_POLICY="${2:-}"
|
|
shift 2
|
|
;;
|
|
--landing-policy)
|
|
LANDING_POLICY="${2:-}"
|
|
shift 2
|
|
;;
|
|
--work-order)
|
|
WORK_ORDER="${2:-}"
|
|
shift 2
|
|
;;
|
|
--landing-branch)
|
|
LANDING_BRANCH_OVERRIDE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--schedule)
|
|
SCHEDULE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--no-require-ai-sane)
|
|
REQUIRE_AI_SANE=false
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
die "unknown arg: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ "$RUN_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "--date must use YYYY-MM-DD"
|
|
[[ "$MAX_CYCLES" =~ ^[0-9]+$ ]] || die "--max-cycles must be a non-negative integer"
|
|
case "$RUNTIME_MODE" in
|
|
auto|direct|stream|tmux|gc) ;;
|
|
*) die "--runtime-mode must be auto, direct, stream, tmux, or gc" ;;
|
|
esac
|
|
|
|
REPO_ROOT="$(resolve_repo_root "$REPO_ROOT")"
|
|
cd "$REPO_ROOT"
|
|
|
|
require_cmd git
|
|
require_cmd jq
|
|
require_cmd ao
|
|
|
|
WORKTREE_STATUS="$(git status --porcelain -uno 2>/dev/null || true)"
|
|
|
|
RUN_STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
RUN_ID="nightly-evolution-${RUN_STAMP}"
|
|
if [[ -z "$OUTPUT_DIR" ]]; then
|
|
OUTPUT_DIR=".agents/nightly/${RUN_DATE}/${RUN_ID}"
|
|
fi
|
|
mkdir -p "$OUTPUT_DIR"
|
|
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
|
|
|
LOCK_DIR="$REPO_ROOT/.agents/nightly/nightly-evolution.lock"
|
|
mkdir -p "$(dirname "$LOCK_DIR")"
|
|
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
die "another nightly evolution run appears active: $LOCK_DIR"
|
|
fi
|
|
trap 'rm -rf "$LOCK_DIR"' EXIT
|
|
|
|
if [[ -f "$REPO_ROOT/.agents/evolve/STOP" || -f "$REPO_ROOT/.agents/rpi/KILL" || -f "$HOME/.config/evolve/KILL" ]]; then
|
|
die "nightly kill switch is present"
|
|
fi
|
|
|
|
MODE="dry-run"
|
|
if [[ "$EXECUTE" == true ]]; then
|
|
MODE="execute"
|
|
fi
|
|
|
|
log "run_id=$RUN_ID mode=$MODE output_dir=$OUTPUT_DIR"
|
|
|
|
if [[ -n "$LANDING_BRANCH_OVERRIDE" ]]; then
|
|
BRANCH="$LANDING_BRANCH_OVERRIDE"
|
|
else
|
|
BRANCH="$(compute_branch "$RUN_DATE")"
|
|
fi
|
|
RUNTIME_TSV="$OUTPUT_DIR/runtime-inventory.tsv"
|
|
split_csv "$RUNNERS" RUNNER_ARRAY
|
|
write_runtime_inventory "$RUNTIME_TSV" "${RUNNER_ARRAY[@]}" "$RUNTIME_CMD" gh bd
|
|
|
|
AI_SANE_JSON="$OUTPUT_DIR/ai-sane.json"
|
|
AI_SANE_ERR="$OUTPUT_DIR/ai-sane.stderr"
|
|
AI_SANE_STATUS="skipped"
|
|
if command -v bushido-box >/dev/null 2>&1; then
|
|
if command -v timeout >/dev/null 2>&1; then
|
|
if run_json_capture "$AI_SANE_JSON" "$AI_SANE_ERR" timeout 20s bushido-box ai-sane --json; then
|
|
AI_SANE_STATUS="$(jq -r '.status // "unknown"' "$AI_SANE_JSON" 2>/dev/null || echo unknown)"
|
|
else
|
|
AI_SANE_STATUS="failed"
|
|
json_empty_object >"$AI_SANE_JSON"
|
|
fi
|
|
elif run_json_capture "$AI_SANE_JSON" "$AI_SANE_ERR" bushido-box ai-sane --json; then
|
|
AI_SANE_STATUS="$(jq -r '.status // "unknown"' "$AI_SANE_JSON" 2>/dev/null || echo unknown)"
|
|
else
|
|
AI_SANE_STATUS="failed"
|
|
json_empty_object >"$AI_SANE_JSON"
|
|
fi
|
|
else
|
|
AI_SANE_STATUS="unavailable"
|
|
json_empty_object >"$AI_SANE_JSON"
|
|
: >"$AI_SANE_ERR"
|
|
fi
|
|
|
|
if [[ "$EXECUTE" == true && "$REQUIRE_AI_SANE" == true && "$AI_SANE_STATUS" != "ok" ]]; then
|
|
die "bushido-box ai-sane is required for execute mode, got: $AI_SANE_STATUS"
|
|
fi
|
|
|
|
DREAM_SETUP_JSON="$OUTPUT_DIR/dream-setup.json"
|
|
DREAM_SETUP_ERR="$OUTPUT_DIR/dream-setup.stderr"
|
|
if ! run_json_capture "$DREAM_SETUP_JSON" "$DREAM_SETUP_ERR" ao overnight setup --json; then
|
|
json_empty_object >"$DREAM_SETUP_JSON"
|
|
fi
|
|
|
|
GH_EVIDENCE="unavailable"
|
|
OPEN_PRS_JSON="$OUTPUT_DIR/open-prs.json"
|
|
OPEN_PRS_ERR="$OUTPUT_DIR/open-prs.stderr"
|
|
BLOCKER_MATRIX_JSON="$OUTPUT_DIR/blocker-matrix.json"
|
|
MAIN_CI_JSON="$OUTPUT_DIR/main-ci-baseline.json"
|
|
if command -v gh >/dev/null 2>&1; then
|
|
if run_json_capture "$OPEN_PRS_JSON" "$OPEN_PRS_ERR" \
|
|
gh pr list --state open --limit 30 --json number,title,headRefName,baseRefName,changedFiles,url,labels; then
|
|
GH_EVIDENCE="ok"
|
|
else
|
|
GH_EVIDENCE="failed"
|
|
json_empty_array >"$OPEN_PRS_JSON"
|
|
fi
|
|
else
|
|
json_empty_array >"$OPEN_PRS_JSON"
|
|
: >"$OPEN_PRS_ERR"
|
|
fi
|
|
|
|
build_blocker_matrix "$OPEN_PRS_JSON" "$BLOCKER_MATRIX_JSON" "$GH_EVIDENCE"
|
|
build_main_ci_baseline "$MAIN_CI_JSON" "$OUTPUT_DIR" "$GH_EVIDENCE"
|
|
MAIN_CI_STATUS="$(jq -r '.status // "unknown"' "$MAIN_CI_JSON")"
|
|
|
|
BRIEF_STATUS="skipped"
|
|
if [[ "$SKIP_BRIEF" != true ]]; then
|
|
if [[ -x "$REPO_ROOT/scripts/nightly-rpi-brief.sh" ]]; then
|
|
if "$REPO_ROOT/scripts/nightly-rpi-brief.sh" --output-dir "$OUTPUT_DIR/nightly-brief" >"$OUTPUT_DIR/nightly-brief.log" 2>&1; then
|
|
BRIEF_STATUS="ok"
|
|
else
|
|
BRIEF_STATUS="failed"
|
|
fi
|
|
else
|
|
BRIEF_STATUS="unavailable"
|
|
fi
|
|
fi
|
|
|
|
SYSTEMD_DIR=""
|
|
if [[ "$EMIT_SYSTEMD" == true ]]; then
|
|
SYSTEMD_DIR="$OUTPUT_DIR/systemd"
|
|
emit_systemd_templates "$SYSTEMD_DIR" "$REPO_ROOT" "$SCHEDULE" "$RUNNERS" "$RUNTIME_CMD" "$MAX_CYCLES"
|
|
fi
|
|
|
|
DREAM_STATUS="not-requested"
|
|
DREAM_PAYLOAD_JSON=""
|
|
DREAM_SUBMIT_JSON=""
|
|
DREAM_SUBMIT_ERR=""
|
|
if [[ "$RUN_DREAM" == true ]]; then
|
|
if [[ "$EXECUTE" != true ]]; then
|
|
DREAM_STATUS="planned"
|
|
else
|
|
DREAM_PAYLOAD_JSON="$OUTPUT_DIR/dream-run-payload.json"
|
|
DREAM_SUBMIT_JSON="$OUTPUT_DIR/dream-submit.json"
|
|
DREAM_SUBMIT_ERR="$OUTPUT_DIR/dream-submit.stderr"
|
|
jq -n \
|
|
--arg dream_run_id "${RUN_ID}-dream" \
|
|
--arg goal "private local nightly Dream before RPI/evolve" \
|
|
--arg output_dir "$OUTPUT_DIR/dream" '
|
|
{
|
|
schema_version: 1,
|
|
job_type: "dream.run",
|
|
dream_run_id: $dream_run_id,
|
|
goal: $goal,
|
|
mode: "daemon",
|
|
output_dir: $output_dir,
|
|
max_iterations: 1
|
|
}' >"$DREAM_PAYLOAD_JSON"
|
|
if ao daemon jobs submit --type dream.run --payload "@$DREAM_PAYLOAD_JSON" --json >"$DREAM_SUBMIT_JSON" 2>"$DREAM_SUBMIT_ERR"; then
|
|
DREAM_STATUS="submitted"
|
|
elif [[ "$SKIP_DREAM_SUBPROCESS" == true ]]; then
|
|
DREAM_STATUS="failed"
|
|
die "Dream daemon submit failed; see $DREAM_SUBMIT_ERR"
|
|
else
|
|
dream_args=(overnight start --warn-only --max-iterations 1 --output-dir "$OUTPUT_DIR/dream")
|
|
dream_args+=(--goal "private local nightly Dream before RPI/evolve")
|
|
for runner in "${RUNNER_ARRAY[@]}"; do
|
|
dream_args+=(--runner "$runner")
|
|
done
|
|
if ao "${dream_args[@]}" >"$OUTPUT_DIR/dream.log" 2>&1; then
|
|
DREAM_STATUS="ok"
|
|
else
|
|
DREAM_STATUS="failed"
|
|
die "Dream phase failed; see $OUTPUT_DIR/dream.log"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
EVOLVE_STATUS="not-requested"
|
|
if [[ "$RUN_EVOLVE" == true ]]; then
|
|
if [[ "$EXECUTE" != true ]]; then
|
|
EVOLVE_STATUS="planned"
|
|
else
|
|
preflight_evolve "$WORK_ORDER" "$REPO_ROOT" "$RUNTIME_CMD" "$GH_EVIDENCE" "$MAIN_CI_STATUS" "$WORKTREE_STATUS" "$LANDING_POLICY" "$BRANCH"
|
|
export AGENTOPS_RPI_RUNTIME_MODE="$RUNTIME_MODE"
|
|
export AGENTOPS_RPI_RUNTIME_COMMAND="$RUNTIME_CMD"
|
|
|
|
# soc-bcrn.3.7: submit to agentopsd via `ao daemon jobs submit` (rpi.run) +
|
|
# wait for terminal status. RPIRunExecutor (sub-wave 5a,
|
|
# cli/internal/daemon/rpi_run.go) handles execution in-process; the prior
|
|
# shell-out wrapper was retired here. Combined output (submit + wait) is
|
|
# captured to $OUTPUT_DIR/evolve.log.
|
|
EVOLVE_PAYLOAD_JSON="$OUTPUT_DIR/evolve-rpi-run-payload.json"
|
|
EVOLVE_SUBMIT_JSON="$OUTPUT_DIR/evolve-submit.json"
|
|
EVOLVE_WAIT_JSON="$OUTPUT_DIR/evolve-wait.json"
|
|
|
|
require_cmd ao
|
|
: >"$OUTPUT_DIR/evolve.log"
|
|
|
|
# Liveness probe: no `ao daemon health` command exists, so a successful
|
|
# `ao daemon jobs list --json` doubles as a daemon-reachability check.
|
|
if ! ao daemon jobs list --json >>"$OUTPUT_DIR/evolve.log" 2>&1; then
|
|
EVOLVE_STATUS="failed"
|
|
die "Evolve phase failed: agentopsd unreachable (ao daemon jobs list); see $OUTPUT_DIR/evolve.log"
|
|
fi
|
|
|
|
# Build the rpi.run payload. Shape: cli/internal/daemon/rpi_jobs.go
|
|
# (RPIRunJobSpec). soc-bcrn.3.8 added the supervisor policy fields
|
|
# (max_cycles, gate_policy, landing_policy, landing_branch) so the
|
|
# daemon path applies the same gates + landing the legacy
|
|
# legacy shell wrapper applied via ao rpi loop --supervisor.
|
|
LANDING_BRANCH_PAYLOAD="${BRANCH:-}"
|
|
jq -n \
|
|
--arg run_id "${RUN_ID}-evolve" \
|
|
--arg goal "private local nightly RPI/evolve cycle" \
|
|
--argjson max_cycles "$MAX_CYCLES" \
|
|
--arg gate_policy "$GATE_POLICY" \
|
|
--arg landing_policy "$LANDING_POLICY" \
|
|
--arg landing_branch "$LANDING_BRANCH_PAYLOAD" '
|
|
{
|
|
schema_version: 1,
|
|
job_type: "rpi.run",
|
|
run_id: $run_id,
|
|
goal: $goal,
|
|
start_phase: 1,
|
|
max_phase: 3,
|
|
test_first: true,
|
|
backend: "gascity-api",
|
|
max_cycles: $max_cycles,
|
|
gate_policy: $gate_policy,
|
|
landing_policy: $landing_policy
|
|
}
|
|
+ (if $landing_branch == "" then {} else {landing_branch: $landing_branch} end)' >"$EVOLVE_PAYLOAD_JSON"
|
|
|
|
if ! ao daemon jobs submit --type rpi.run --payload "@$EVOLVE_PAYLOAD_JSON" --json \
|
|
>"$EVOLVE_SUBMIT_JSON" 2>>"$OUTPUT_DIR/evolve.log"; then
|
|
cat "$EVOLVE_SUBMIT_JSON" >>"$OUTPUT_DIR/evolve.log" 2>/dev/null || true
|
|
EVOLVE_STATUS="failed"
|
|
die "Evolve phase failed: ao daemon jobs submit (rpi.run); see $OUTPUT_DIR/evolve.log"
|
|
fi
|
|
cat "$EVOLVE_SUBMIT_JSON" >>"$OUTPUT_DIR/evolve.log"
|
|
|
|
EVOLVE_JOB_ID="$(jq -r '.job_id // empty' "$EVOLVE_SUBMIT_JSON")"
|
|
if [[ -z "$EVOLVE_JOB_ID" ]]; then
|
|
EVOLVE_STATUS="failed"
|
|
die "Evolve phase failed: no job_id in submit response; see $OUTPUT_DIR/evolve.log"
|
|
fi
|
|
|
|
if ! ao --output json daemon jobs wait "$EVOLVE_JOB_ID" \
|
|
>"$EVOLVE_WAIT_JSON" 2>>"$OUTPUT_DIR/evolve.log"; then
|
|
cat "$EVOLVE_WAIT_JSON" >>"$OUTPUT_DIR/evolve.log" 2>/dev/null || true
|
|
EVOLVE_STATUS="failed"
|
|
die "Evolve phase failed: ao daemon jobs wait $EVOLVE_JOB_ID; see $OUTPUT_DIR/evolve.log"
|
|
fi
|
|
cat "$EVOLVE_WAIT_JSON" >>"$OUTPUT_DIR/evolve.log"
|
|
|
|
EVOLVE_TERMINAL_STATUS="$(jq -r '.status // empty' "$EVOLVE_WAIT_JSON")"
|
|
if [[ "$EVOLVE_TERMINAL_STATUS" == "completed" ]]; then
|
|
EVOLVE_STATUS="ok"
|
|
else
|
|
EVOLVE_STATUS="failed"
|
|
die "Evolve phase failed: rpi.run job $EVOLVE_JOB_ID terminal status '$EVOLVE_TERMINAL_STATUS'; see $OUTPUT_DIR/evolve.log"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
DIGEST_JSON="$OUTPUT_DIR/digest.json"
|
|
DIGEST_MD="$OUTPUT_DIR/digest.md"
|
|
|
|
jq -n \
|
|
--arg schema_version "1" \
|
|
--arg run_id "$RUN_ID" \
|
|
--arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
--arg mode "$MODE" \
|
|
--arg repo_root "$REPO_ROOT" \
|
|
--arg run_date "$RUN_DATE" \
|
|
--arg branch "$BRANCH" \
|
|
--arg runners "$RUNNERS" \
|
|
--arg runtime_cmd "$RUNTIME_CMD" \
|
|
--arg runtime_mode "$RUNTIME_MODE" \
|
|
--arg ai_sane_status "$AI_SANE_STATUS" \
|
|
--arg brief_status "$BRIEF_STATUS" \
|
|
--arg dream_status "$DREAM_STATUS" \
|
|
--arg evolve_status "$EVOLVE_STATUS" \
|
|
--arg output_dir "$OUTPUT_DIR" \
|
|
--arg systemd_dir "$SYSTEMD_DIR" \
|
|
--arg dream_payload_json "$DREAM_PAYLOAD_JSON" \
|
|
--arg dream_submit_json "$DREAM_SUBMIT_JSON" \
|
|
--arg dream_submit_err "$DREAM_SUBMIT_ERR" \
|
|
--slurpfile ai "$AI_SANE_JSON" \
|
|
--slurpfile dreamSetup "$DREAM_SETUP_JSON" \
|
|
--slurpfile openPrs "$OPEN_PRS_JSON" \
|
|
--slurpfile blockerMatrix "$BLOCKER_MATRIX_JSON" \
|
|
--slurpfile mainCi "$MAIN_CI_JSON" \
|
|
--arg gh_evidence "$GH_EVIDENCE" \
|
|
--rawfile runtimeInventory "$RUNTIME_TSV" '
|
|
{
|
|
schema_version: ($schema_version | tonumber),
|
|
run_id: $run_id,
|
|
generated_at: $generated_at,
|
|
mode: $mode,
|
|
repo_root: $repo_root,
|
|
run_date: $run_date,
|
|
planned_branch: $branch,
|
|
runners: ($runners | split(",")),
|
|
runtime: {
|
|
command: $runtime_cmd,
|
|
mode: $runtime_mode,
|
|
inventory_tsv: $runtimeInventory
|
|
},
|
|
readiness: {
|
|
ai_sane_status: $ai_sane_status,
|
|
ai_sane: ($ai[0] // {}),
|
|
dream_setup: ($dreamSetup[0] // {})
|
|
},
|
|
github: {
|
|
open_prs: ($openPrs[0] // [])
|
|
},
|
|
admission_context: {
|
|
gh_evidence: $gh_evidence,
|
|
blocker_matrix: ($blockerMatrix[0] // {}),
|
|
main_ci_baseline: ($mainCi[0] // {})
|
|
},
|
|
phases: {
|
|
nightly_brief: $brief_status,
|
|
dream: $dream_status,
|
|
evolve: $evolve_status
|
|
},
|
|
artifacts: {
|
|
output_dir: $output_dir,
|
|
digest_json: ($output_dir + "/digest.json"),
|
|
digest_md: ($output_dir + "/digest.md"),
|
|
dream_payload: (if $dream_payload_json == "" then null else $dream_payload_json end),
|
|
dream_submit_json: (if $dream_submit_json == "" then null else $dream_submit_json end),
|
|
dream_submit_stderr: (if $dream_submit_err == "" then null else $dream_submit_err end),
|
|
systemd_dir: (if $systemd_dir == "" then null else $systemd_dir end)
|
|
}
|
|
}' >"$DIGEST_JSON"
|
|
|
|
write_markdown_digest "$DIGEST_MD" "$RUN_ID" "$MODE" "$BRANCH" "$AI_SANE_STATUS" "$DREAM_STATUS" "$EVOLVE_STATUS" "$SYSTEMD_DIR" "$GH_EVIDENCE" "$MAIN_CI_STATUS"
|
|
|
|
log "digest=$DIGEST_JSON"
|
|
|
|
PR_DIGEST_SCRIPT="$REPO_ROOT/scripts/nightly-pr-digest.sh"
|
|
if [[ -x "$PR_DIGEST_SCRIPT" ]]; then
|
|
PR_DIGEST_MD="$OUTPUT_DIR/pr-body.md"
|
|
if "$PR_DIGEST_SCRIPT" --run-dir "$OUTPUT_DIR" --branch "$BRANCH" --output "$PR_DIGEST_MD" 2>"$OUTPUT_DIR/pr-digest.stderr"; then
|
|
log "pr_digest=$PR_DIGEST_MD"
|
|
else
|
|
log "pr_digest=failed (see $OUTPUT_DIR/pr-digest.stderr)"
|
|
fi
|
|
fi
|
|
|
|
log "done"
|