From a636cd526ba5a72359a27cd0be5843a382cc646a Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 05:49:54 +0000 Subject: [PATCH 1/7] fix(core): mirrored-source walks the canonical root (WI-1071) The collector canonicalised the root for the relative_to comparisons but walked the stored spelling, so a root under a symlinked parent or with a '..' in it reported manifests as absolute paths. Walk the resolved root, as Python's collector does (4aa81f3), and port its two regression tests. Co-Authored-By: Claude Haiku 5.5 Signed-off-by: sprooty --- engine/core/src/collectors/mirrored_source.rs | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/engine/core/src/collectors/mirrored_source.rs b/engine/core/src/collectors/mirrored_source.rs index 13af2681c..65be02ac2 100644 --- a/engine/core/src/collectors/mirrored_source.rs +++ b/engine/core/src/collectors/mirrored_source.rs @@ -135,7 +135,9 @@ impl Collector for MirroredSourceCollector { }) .collect(); let mut found = Vec::new(); - for path in walk_project(&root, &project.exclusions, &[], MAX_FILES) { + // Walked from the canonical root, so every path below it is relative to + // the same prefix `strip_prefix` removes (WI-1071, Python 4aa81f3). + for path in walk_project(&resolved_root, &project.exclusions, &[], MAX_FILES) { let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { continue; }; @@ -382,6 +384,76 @@ mod tests { root } + #[test] + fn a_root_under_a_symlinked_parent_yields_normal_findings() { + // WI-1071: a /home -> /data style root used to fail the whole project. + let base = scratch("symlinked-parent"); + let real = base.join("data"); + crate_at( + &real.join("rustnzb").join("crates").join("nzb-core"), + "nzb-core", + "0.3.0", + ); + let link = base.join("home"); + std::os::unix::fs::symlink(&real, &link).unwrap(); + let published = base.join("nzb-core"); + crate_at(&published, "nzb-core", "0.4.0"); + + let found = collect( + vec![RegisteredProject { + id: "prj_published".to_string(), + slug: "nzb-core".to_string(), + root_path: published.display().to_string(), + repo_url: None, + }], + &link.join("rustnzb"), + "rustnzb", + ); + assert_eq!( + found + .iter() + .map(|f| f.subject_key.as_str()) + .collect::>(), + vec!["mirror:rustnzb/crates/nzb-core->nzb-core"] + ); + assert_eq!(found[0].payload["local_path"], "crates/nzb-core"); + assert_eq!(found[0].payload["manifest"], "crates/nzb-core/Cargo.toml"); + } + + #[test] + fn a_root_stored_with_dot_dot_yields_normal_findings() { + // WI-1071: a root stored non-canonically, with `..` in it. + let base = scratch("dot-dot"); + let vendoring = base.join("rustnzb"); + crate_at( + &vendoring.join("crates").join("nzb-core"), + "nzb-core", + "0.3.0", + ); + std::fs::create_dir_all(base.join("elsewhere")).unwrap(); + let published = base.join("nzb-core"); + crate_at(&published, "nzb-core", "0.4.0"); + + let found = collect( + vec![RegisteredProject { + id: "prj_published".to_string(), + slug: "nzb-core".to_string(), + root_path: published.display().to_string(), + repo_url: None, + }], + &base.join("elsewhere").join("..").join("rustnzb"), + "rustnzb", + ); + assert_eq!( + found + .iter() + .map(|f| f.subject_key.as_str()) + .collect::>(), + vec!["mirror:rustnzb/crates/nzb-core->nzb-core"] + ); + assert_eq!(found[0].payload["manifest"], "crates/nzb-core/Cargo.toml"); + } + #[test] fn a_vendored_crate_that_is_also_a_project_is_reported() { let base = scratch("vendored"); From c8b3e9a634fedc2decd7bfdf4c713f4a4af69f7f Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 05:49:54 +0000 Subject: [PATCH 2/7] feat(core): sweep runs deploy-lanes and announces CI conclusions (WI-1071) The deploy-lanes collector was already ported but the sweep refused while a lane was configured. Register it beside the forge reads, with a real version fetch, and drop the refusal. Port the bound-branch CI announce from ci_watch.py into application/ci_watch.rs and call it after the sweep publishes, when forge-checks ran. A failure is logged, never returned, as Python contains it. The Python test_ci_watch row is unported until the sweep-containment test is ported. Co-Authored-By: Claude Haiku 5.5 Signed-off-by: sprooty --- docs/PYTHON_TEST_DISPOSITION.md | 2 +- engine/core/src/adapters/forge/lanes.rs | 39 ++ engine/core/src/adapters/forge/mod.rs | 7 +- engine/core/src/application/ci_watch.rs | 590 ++++++++++++++++++++++++ engine/core/src/application/collect.rs | 125 +++-- engine/core/src/application/mod.rs | 1 + engine/core/src/core.rs | 2 +- 7 files changed, 729 insertions(+), 37 deletions(-) create mode 100644 engine/core/src/application/ci_watch.rs diff --git a/docs/PYTHON_TEST_DISPOSITION.md b/docs/PYTHON_TEST_DISPOSITION.md index d9a99f92c..b85174b2c 100644 --- a/docs/PYTHON_TEST_DISPOSITION.md +++ b/docs/PYTHON_TEST_DISPOSITION.md @@ -41,7 +41,7 @@ gain tests; it does not move down. | test_check_rust_version.py | retired | `scripts/check_rust_version.py`. A Python lint over the engine toolchain pin. | | test_check_workflow_policy.py | retired | `scripts/check_workflow_policy.py` over GitHub workflow YAML. CI policy, not product behaviour. | | test_checks.py | ported | CI-revision reads in the collectors and the project brief. | -| test_ci_watch.py | ported | Watched-ref and deployed-version reads in `application/deployed.rs`. | +| test_ci_watch.py | unported | Watched-ref and deployed-version reads are in `application/deployed.rs`. The bound-branch announce is in `application/ci_watch.rs`: the feed event once per conclusion, staleness, and the notice text. Still unported, owner WI-1071: `test_a_sweep_announces_and_survives_an_announce_failure`, which needs an announce failure injected into a sweep. The containment is in `application/collect.rs` and logs rather than failing. | | test_claude_profile.py | unported | Claude Code account profiles and their bootstrap. Named accounts land on `session.start` in WI-1306 (triage row 7782a7e8, under WI-1074). | | test_cli.py | ported | `adapters/cli.rs`. | | test_clone.py | ported | Clone service under `application/services`. | diff --git a/engine/core/src/adapters/forge/lanes.rs b/engine/core/src/adapters/forge/lanes.rs index c1558b16b..1e2a24b15 100644 --- a/engine/core/src/adapters/forge/lanes.rs +++ b/engine/core/src/adapters/forge/lanes.rs @@ -12,6 +12,9 @@ //! whose source cannot be read records *why* in its observation rather than //! failing the sweep, so "not readable" never reads as "not deployed". +use std::io::Read; +use std::time::Duration; + use serde_json::{Map, Value}; use super::kinds::{COLLECTOR_DEPLOY_LANES, KIND_DEPLOY_LANE}; @@ -33,6 +36,42 @@ pub trait VersionFetch { fn get(&self, url: &str) -> Result<(u16, Vec), String>; } +/// The real fetch: one GET with the collector's ten-second timeout, reading at +/// most 64 KiB. An error status answers with an empty body, as Python does. +pub struct HttpVersionFetch; + +const VERSION_TIMEOUT: Duration = Duration::from_secs(10); +const VERSION_BODY_CAP: u64 = 65_536; + +impl VersionFetch for HttpVersionFetch { + fn get(&self, url: &str) -> Result<(u16, Vec), String> { + if !(url.starts_with("https://") || url.starts_with("http://")) { + return Err("version_url must be http(s)".to_owned()); + } + let agent = ureq::AgentBuilder::new().timeout(VERSION_TIMEOUT).build(); + let request = agent + .get(url) + .set("Accept", "application/json") + .set("User-Agent", "vogt-deploy-lanes"); + match request.call() { + Ok(response) => read_capped(response), + Err(ureq::Error::Status(status, _)) => Ok((status, Vec::new())), + Err(error) => Err(error.to_string()), + } + } +} + +fn read_capped(response: ureq::Response) -> Result<(u16, Vec), String> { + let status = response.status(); + let mut bytes = Vec::new(); + response + .into_reader() + .take(VERSION_BODY_CAP) + .read_to_end(&mut bytes) + .map_err(|error| error.to_string())?; + Ok((status, bytes)) +} + /// One observation per configured deployment lane of a project. pub struct DeployLanesCollector<'a> { directory: &'a dyn ForgeDirectory, diff --git a/engine/core/src/adapters/forge/mod.rs b/engine/core/src/adapters/forge/mod.rs index 992b0e632..3a080363b 100644 --- a/engine/core/src/adapters/forge/mod.rs +++ b/engine/core/src/adapters/forge/mod.rs @@ -37,10 +37,11 @@ pub use collectors::{ pub use directory::ForgeClients; pub use github::repo_of; pub use kinds::{ - current_collector, COLLECTOR_CHECKS, COLLECTOR_ISSUES, COLLECTOR_NOTIFICATIONS, - COLLECTOR_PULLS, KIND_CHECK, KIND_DEPLOY_LANE, KIND_ISSUE, KIND_LABEL, KIND_NOTIFICATION, - KIND_PULL_REQUEST, KIND_RELEASE, + current_collector, COLLECTOR_CHECKS, COLLECTOR_DEPLOY_LANES, COLLECTOR_ISSUES, + COLLECTOR_NOTIFICATIONS, COLLECTOR_PULLS, KIND_CHECK, KIND_DEPLOY_LANE, KIND_ISSUE, KIND_LABEL, + KIND_NOTIFICATION, KIND_PULL_REQUEST, KIND_RELEASE, }; +pub use lanes::{DeployLanesCollector, HttpVersionFetch}; pub use models::RepoRef; pub use provider::ForgeProvider; pub use registry::has_configured_forge; diff --git a/engine/core/src/application/ci_watch.rs b/engine/core/src/application/ci_watch.rs new file mode 100644 index 000000000..9c0badd54 --- /dev/null +++ b/engine/core/src/application/ci_watch.rs @@ -0,0 +1,590 @@ +//! CI watching on the branches work items are bound to. Ports +//! `services/ci_watch.py`'s announce (WI-855). +//! +//! A branch bound to a work item is watched: the `forge-checks` sweep observes +//! its runs, and once every workflow on the branch's newest revision has +//! concluded this module +//! +//! - leaves the Inbox entry that `inbox.rs` builds from the same branches, and +//! - after the sweep, publishes one `ci.branch_concluded` event on the declared +//! feed and types a one-line notice into each live session started for the +//! item, so the agent wakes on the result. +//! +//! "Once" is the event: its entity id names the item, branch, revision and +//! verdict, so a conclusion that already has an event is never re-announced, +//! not by the next sweep and not by a restart. Only fresh conclusions (the last +//! run finished within [`FRESH_WINDOW_SECONDS`]) are announced, so binding an +//! old branch never floods a session with history. + +use std::collections::BTreeMap; + +use serde_json::{json, Value}; + +use crate::adapters::engine::EngineClient; +use crate::adapters::forge::KIND_CHECK; +use crate::application::context::AppContext; +use crate::core::{Clock, IdFactory, Moment, Observation, WorkOverlay, TERMINAL_STATES}; +use crate::decisions::{self, BranchCi, BranchState}; +use crate::errors::VogtError; +use crate::storage::interface::{DeclaredStore, ObservedStore, ReadView}; + +/// `ci_watch.py` `CI_BRANCH_CONCLUDED_EVENT`. +pub const CI_BRANCH_CONCLUDED_EVENT: &str = "ci.branch_concluded"; +/// How recent a conclusion must be to be announced to a session: six hours. +const FRESH_WINDOW_SECONDS: i64 = 6 * 3600; +/// Bound overlays considered per read. Python's `ci_watch.MAX_BOUND`. +const MAX_BOUND: i64 = 500; +/// Live sessions nudged per conclusion. Python's `ci_watch.MAX_SESSIONS`. +const MAX_SESSIONS: i64 = 10; +/// Python's `ci_watch.NOTICE_MAX_CHARS`, counted in characters. +const NOTICE_MAX_CHARS: usize = 400; +/// The check observations read per project for the announce. Python's limit. +const CHECKS_PER_PROJECT: i64 = 2000; + +/// One branch a still-open work item is bound to, and its CI. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BoundBranch { + pub work_item_ref: String, + pub work_item_id: Option, + pub project_id: String, + pub branch: String, + pub ci: BranchCi, +} + +impl BoundBranch { + /// `ci_watch.py` `BoundBranch.event_entity`: the event's identity. + pub fn event_entity(&self) -> String { + format!( + "{}:{}@{}:{}", + self.work_item_ref, + self.branch, + self.ci.revision, + state_word(self.ci.state) + ) + } +} + +/// The lower-case word Python's `BranchCi.state` carries. +fn state_word(state: BranchState) -> &'static str { + match state { + BranchState::Running => "running", + BranchState::Passed => "passed", + BranchState::Failed => "failed", + BranchState::Cancelled => "cancelled", + } +} + +/// The sweep's clock, read once per use as Python's `ctx.clock()` is. +fn moment_now(ctx: &AppContext) -> Moment { + let mut clock = ctx + .clock + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + clock.now() +} + +/// A conclusion is settled once no run is still going. +fn settled(ci: &BranchCi) -> bool { + matches!( + ci.state, + BranchState::Passed | BranchState::Failed | BranchState::Cancelled + ) +} + +/// CI on the newest revision of every branch an open item is bound to. +/// +/// `checks_by_branch` is keyed `(project_id, branch)`, built once from the +/// checks the caller already holds, so nothing here re-reads observations per +/// branch. An item's state is read only for an overlay with observed runs. +pub fn bound_branches( + view: &impl ReadView, + checks_by_branch: &BTreeMap<(String, String), Vec>, +) -> Result, VogtError> { + let mut found = Vec::new(); + for overlay in view.bound_branch_overlays(MAX_BOUND)? { + let settled_branches = settled_on(&overlay, checks_by_branch); + if settled_branches.is_empty() { + continue; + } + let item = view.work_item_by_ref(&overlay.subject_key)?; + let state = match &item { + Some(item) => Some(item.state.as_str().to_owned()), + None => overlay.workflow_state.clone(), + }; + if state + .as_deref() + .is_some_and(|state| TERMINAL_STATES.contains(&state)) + { + continue; + } + for (branch, ci) in settled_branches { + found.push(BoundBranch { + work_item_ref: overlay.subject_key.clone(), + work_item_id: item.as_ref().map(|item| item.id.clone()), + project_id: overlay.project_id.clone(), + branch, + ci, + }); + } + } + Ok(found) +} + +/// The branches of one overlay that have runs observed, with their CI. +fn settled_on( + overlay: &WorkOverlay, + checks_by_branch: &BTreeMap<(String, String), Vec>, +) -> Vec<(String, BranchCi)> { + let mut found = Vec::new(); + for branch in &overlay.branches { + let Some(runs) = checks_by_branch.get(&(overlay.project_id.clone(), branch.clone())) else { + continue; + }; + if let Some(ci) = decisions::branch_ci(runs, branch) { + found.push((branch.clone(), ci)); + } + } + found +} + +/// Check observations keyed `(project_id, branch)`. An observation without a +/// project or a branch is not CI on any branch, so it is skipped. +pub fn index_by_branch<'a>( + checks: impl IntoIterator, +) -> BTreeMap<(String, String), Vec> { + let mut index: BTreeMap<(String, String), Vec> = BTreeMap::new(); + for check in checks { + let Some(project_id) = check.project_id.clone() else { + continue; + }; + let Some(branch) = check.payload.get("branch").and_then(Value::as_str) else { + continue; + }; + if branch.is_empty() { + continue; + } + index + .entry((project_id, branch.to_owned())) + .or_default() + .push(check.clone()); + } + index +} + +/// The one line typed into a bound session. Plain text, no control characters: +/// it is data for the agent, not a command. +pub fn notice_text(bound: &BoundBranch) -> String { + let ci = &bound.ci; + let verdict = match ci.state { + BranchState::Passed => "PASSED".to_owned(), + BranchState::Failed => "FAILED".to_owned(), + BranchState::Cancelled => "CANCELLED".to_owned(), + BranchState::Running => "RUNNING".to_owned(), + }; + let detail = if ci.failing.is_empty() { + format!("; {} workflow(s)", ci.runs.len()) + } else { + format!("; failing: {}", ci.failing.join(", ")) + }; + let link = ci + .runs + .iter() + .find(|run| ci.failing.contains(&run.workflow) && run.observation.source_url.is_some()) + .and_then(|run| run.observation.source_url.clone()); + let tail = link.map(|link| format!(" — {link}")).unwrap_or_default(); + let short: String = ci.revision.chars().take(12).collect(); + let line = format!( + "[vogt] CI {verdict} on {} @ {short} ({}){detail}{tail}", + bound.branch, bound.work_item_ref + ); + // Workflow names and URLs come from the forge: strip every character that + // is not printable (an Esc or a carriage return would be a keystroke, not + // text) and bound the length, so the line can only ever be read. + line.chars() + .filter(|ch| crate::core::py_printable(*ch)) + .take(NOTICE_MAX_CHARS) + .collect() +} + +/// Whether a conclusion finished recently enough to announce. `concluded_at` +/// must carry an offset, as Python's `fromisoformat` check requires: a naive +/// timestamp is never fresh. +fn fresh(concluded_at: Option<&str>, now_unix: i64) -> bool { + let Some(text) = concluded_at else { + return false; + }; + let Ok(moment) = chrono::DateTime::parse_from_rfc3339(text) else { + return false; + }; + moment.timestamp() >= now_unix - FRESH_WINDOW_SECONDS +} + +/// Announce fresh, not-yet-announced bound-branch conclusions. +/// +/// Called after a sweep's projections are rebuilt. Returns how many +/// conclusions were announced. An engine that cannot take the notice is not an +/// error here: the Inbox entry already says the fact, so the sweep goes on. +pub fn announce_concluded( + ctx: &AppContext, +) -> Result { + let found = { + let view = ctx.declared.read()?; + let mut projects: Vec = view + .bound_branch_overlays(MAX_BOUND)? + .into_iter() + .map(|overlay| overlay.project_id) + .collect(); + projects.sort(); + projects.dedup(); + if projects.is_empty() { + return Ok(0); + } + let mut checks: Vec = Vec::new(); + for project_id in &projects { + checks.extend(ctx.observed.latest( + &[KIND_CHECK.to_owned()], + Some(project_id.as_str()), + false, + false, + CHECKS_PER_PROJECT, + )?); + } + bound_branches(&view, &index_by_branch(&checks))? + }; + let mut announced = 0; + for bound in found { + let now = moment_now(ctx).unix_seconds(); + if !settled(&bound.ci) || !fresh(bound.ci.concluded_at.as_deref(), now) { + continue; + } + let entity = bound.event_entity(); + let sessions: Vec = { + let view = ctx.declared.read()?; + if !view.list_events(0, 1, Some(entity.as_str()))?.is_empty() { + continue; + } + match &bound.work_item_id { + None => Vec::new(), + Some(work_item_id) => view + .list_sessions(None, Some(work_item_id), false, MAX_SESSIONS, 0)? + .into_iter() + .map(|session| session.engine_session_id) + .collect(), + } + }; + let nudged = nudge( + ctx.engine.as_ref(), + ctx.config.ci_watch_notify_sessions, + &sessions, + &bound, + )?; + ctx.declared.publish_event( + CI_BRANCH_CONCLUDED_EVENT, + "work_item_branch", + &entity, + &json!({ + "work_item": bound.work_item_ref, + "branch": bound.branch, + "revision": bound.ci.revision, + "state": state_word(bound.ci.state), + "failing": bound.ci.failing, + "sessions_notified": nudged, + }), + moment_now(ctx), + )?; + announced += 1; + } + Ok(announced) +} + +/// Type the notice and then Enter into each session. A session at a permission +/// prompt refuses the input (WI-983): that is a person's answer to give, so the +/// session hears about the conclusion the next time it reads the Inbox. +fn nudge( + engine: Option<&EngineClient>, + notify: bool, + engine_ids: &[String], + bound: &BoundBranch, +) -> Result { + let Some(engine) = engine.filter(|_| notify) else { + return Ok(0); + }; + let text = notice_text(bound); + let mut sent = 0; + for engine_id in engine_ids { + // The text, then the Enter that submits it. The Enter is sent only when + // the text was taken. + let delivered = || -> Result { + Ok(engine.send_input(engine_id, &text, false, false)? + && engine.send_input(engine_id, "", true, false)?) + }; + match delivered() { + Ok(true) => sent += 1, + Ok(false) => {} + Err(VogtError::PersonRequired(_)) => { + tracing::info!(target: "vogt.ci_watch", session = %engine_id, "not notifying a session at a permission prompt of a CI conclusion"); + } + Err(VogtError::EngineUnavailable(error)) => { + tracing::warn!(target: "vogt.ci_watch", session = %engine_id, error = %error, "could not notify a bound session of a CI conclusion"); + } + Err(error) => return Err(error), + } + } + Ok(sent) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::adapters::engine::{EngineClient, Transport}; + use crate::adapters::forge::{COLLECTOR_CHECKS, KIND_CHECK}; + use crate::application::context::{build_context, Built}; + use crate::application::projects::register_op; + use crate::application::services::work::{bind_branch_op, create_op}; + use crate::config::VogtConfig; + use crate::core::{ActorKind, Moment, Principal, SequentialIds, StepClock, SweepOutcome}; + use crate::storage::observed_types::PendingObservation; + use std::sync::{Arc, Mutex}; + + fn unique_dir(label: &str) -> std::path::PathBuf { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + let dir = std::env::temp_dir().join(format!( + "vogt-ci-watch-{label}-{}-{}", + std::process::id(), + COUNTER.fetch_add(1, Ordering::Relaxed) + )); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + /// A step-clock instance with one registered project `app` and one work + /// item `WI-1` bound to branch `wi-1`. Nothing else is configured: no engine + /// and no forge, so the announce reaches the declared feed only. + fn bound_instance(label: &str, engine: Option) -> Built { + let dir = unique_dir(label); + let mut clock = Some(StepClock::new(Moment::from_unix(1_700_000_000, 0))); + let mut ids = Some(SequentialIds::new(None).unwrap()); + crate::application::instance::init(&dir, &mut clock, &mut ids).unwrap(); + let config = VogtConfig { + data_dir: dir.clone(), + ..VogtConfig::default() + }; + let built = build_context( + config, + Some(Principal::new("local:test-user", ActorKind::Human, "Test").unwrap()), + clock, + ids, + None, + engine, + None, + None, + ) + .unwrap(); + register_op( + &built, + json!({"name": "app", "root_path": dir.to_string_lossy(), "reason": "test"}), + ) + .unwrap(); + create_op( + &built, + json!({"kind": "chore", "title": "Watch me", "project": "app", "local_only": true, "reason": "test"}), + ) + .unwrap(); + bind_branch_op( + &built, + json!({"ref": "WI-1", "branch": "wi-1", "reason": "test"}), + ) + .unwrap(); + built + } + + fn context(built: &Built) -> &AppContext { + match built { + Built::StepSequential(ctx) => ctx, + _ => unreachable!("bound_instance builds a step clock"), + } + } + + /// A check observation on `wi-1`, stamped `updated_at`, sealed as one sweep. + fn seed( + ctx: &AppContext, + checks: &[(&str, Option<&str>)], + updated_at: &str, + ) { + let project_id = ctx + .declared + .read() + .unwrap() + .list_projects(10, 0) + .unwrap() + .into_iter() + .next() + .unwrap() + .id; + let at = moment_now(ctx); + let findings: Vec = checks + .iter() + .map(|(name, conclusion)| PendingObservation { + kind: KIND_CHECK.to_owned(), + subject_key: format!("ci:acme/app@{}:{name}@wi-1", "b".repeat(40)), + payload: json!({ + "revision": "b".repeat(40), + "check": name, + "conclusion": conclusion, + "status": if conclusion.is_some() { "completed" } else { "in_progress" }, + "branch": "wi-1", + "event": "pull_request", + "updated_at": updated_at, + }), + content_digest: format!("{name}-{conclusion:?}-{updated_at}"), + project_id: Some(project_id.clone()), + source_url: Some(format!("https://github.com/acme/app/actions/runs/{name}")), + promoted: false, + }) + .collect(); + let row = ctx + .observed + .begin_sweep(COLLECTOR_CHECKS, std::slice::from_ref(&project_id), at) + .unwrap(); + ctx.observed.append(&row.id, &findings, at).unwrap(); + ctx.observed + .finish_sweep(&row.id, SweepOutcome::Ok, &[("projects", 1)], at, None) + .unwrap(); + ctx.observed.rebuild_latest().unwrap(); + } + + fn stamp(ctx: &AppContext) -> String { + let now = moment_now(ctx).unix_seconds(); + chrono::DateTime::from_timestamp(now - 300, 0) + .unwrap() + .to_rfc3339() + } + + fn concluded_events(built: &Built) -> Vec { + let ctx = context(built); + let view = ctx.declared.read().unwrap(); + view.list_events(0, 500, None) + .unwrap() + .into_iter() + .filter(|event| event.kind == CI_BRANCH_CONCLUDED_EVENT) + .map(|event| event.summary) + .collect() + } + + /// A transport that records each engine request and answers 200 `{"ok":true}`. + fn recording_engine() -> (Arc>>, EngineClient) { + let seen = Arc::new(Mutex::new(Vec::new())); + let record = Arc::clone(&seen); + let transport: Transport = Arc::new(move |url, _headers, body, method| { + record + .lock() + .unwrap() + .push(format!("{method} {url} {}", String::from_utf8_lossy(body))); + (200, br#"{"ok":true}"#.to_vec()) + }); + ( + seen, + EngineClient::new("http://engine", Some("tok".to_string()), Some(transport)), + ) + } + + fn settled_ci(state: BranchState, failing: &[&str]) -> BranchCi { + BranchCi { + branch: "wi-9".to_owned(), + revision: "a".repeat(40), + state, + runs: Vec::new(), + failing: failing.iter().map(|name| (*name).to_owned()).collect(), + concluded_at: Some("2026-10-04T00:00:00Z".to_owned()), + } + } + + #[test] + fn a_fresh_conclusion_is_one_with_an_offset_inside_the_window() { + let now = 1_700_000_000; + let inside = chrono::DateTime::from_timestamp(now - 3600, 0) + .unwrap() + .to_rfc3339(); + let outside = chrono::DateTime::from_timestamp(now - FRESH_WINDOW_SECONDS - 60, 0) + .unwrap() + .to_rfc3339(); + assert!(fresh(Some(&inside), now)); + assert!(!fresh(Some(&outside), now), "older than the window"); + assert!( + !fresh(Some("2023-11-14T22:13:20"), now), + "naive is never fresh" + ); + assert!(!fresh(Some("not a time"), now)); + assert!(!fresh(None, now)); + } + + #[test] + fn notice_text_is_one_plain_line() { + let text = notice_text(&BoundBranch { + work_item_ref: "WI-9".to_owned(), + work_item_id: None, + project_id: "prj_1".to_owned(), + branch: "wi-9".to_owned(), + ci: settled_ci(BranchState::Passed, &[]), + }); + assert!( + text.starts_with("[vogt] CI PASSED on wi-9 @ aaaaaaaaaaaa (WI-9)"), + "{text}" + ); + assert!(!text.contains('\n') && !text.contains('\r')); + + let hostile = notice_text(&BoundBranch { + work_item_ref: "WI-9".to_owned(), + work_item_id: None, + project_id: "prj_1".to_owned(), + branch: "wi-9".to_owned(), + ci: settled_ci(BranchState::Failed, &["ci\u{1b}[2J\rrm -rf"]), + }); + assert!( + !hostile.contains('\u{1b}') && !hostile.contains('\r'), + "{hostile:?}" + ); + assert!(hostile.chars().count() <= NOTICE_MAX_CHARS); + } + + #[test] + fn a_bound_branch_conclusion_reaches_the_feed_once() { + let (seen, engine) = recording_engine(); + let built = bound_instance("once", Some(engine)); + let ctx = context(&built); + let at = stamp(ctx); + + seed(ctx, &[("ci", None), ("lint", Some("success"))], &at); + assert_eq!(announce_concluded(ctx).unwrap(), 0, "still running"); + assert!(concluded_events(&built).is_empty()); + + seed( + ctx, + &[("ci", Some("failure")), ("lint", Some("success"))], + &at, + ); + assert_eq!(announce_concluded(ctx).unwrap(), 1); + let events = concluded_events(&built); + assert_eq!(events.len(), 1, "{events:?}"); + assert_eq!(events[0]["state"], "failed"); + assert_eq!(events[0]["failing"], json!(["ci"])); + assert_eq!(events[0]["work_item"], "WI-1"); + // No session is started for the item, so nothing is typed into one. + assert_eq!(events[0]["sessions_notified"], 0); + assert!(seen.lock().unwrap().is_empty()); + + assert_eq!(announce_concluded(ctx).unwrap(), 0, "once per conclusion"); + assert_eq!(concluded_events(&built).len(), 1); + } + + #[test] + fn a_stale_conclusion_is_never_announced() { + let built = bound_instance("stale", None); + let ctx = context(&built); + seed(ctx, &[("ci", Some("success"))], "2000-01-01T00:00:00Z"); + assert_eq!(announce_concluded(ctx).unwrap(), 0); + assert!(concluded_events(&built).is_empty()); + } +} diff --git a/engine/core/src/application/collect.rs b/engine/core/src/application/collect.rs index a81cd3a49..c2e46052e 100644 --- a/engine/core/src/application/collect.rs +++ b/engine/core/src/application/collect.rs @@ -8,9 +8,10 @@ //! agent-activity when transcript roots are, and when a forge is configured the //! forge sync pair (forge-issues, forge-prs) and the five forge read collectors //! (forge-checks, forge-releases, forge-labels, forge-posture, -//! forge-notifications). Contract-checker runs only when named. Deploy-lanes is -//! not ported: a sweep refuses while a lane is configured rather than answering -//! as if it did not exist. CI watch is not started here. +//! forge-notifications), and deploy-lanes when a lane is configured. Contract- +//! checker runs only when named. Once the sweep has published its events, the +//! CI-watch announce runs after a checks collector, and its failure is logged, +//! never returned: a sweep whose evidence landed does not fail for a nudge. use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::{Component, Path, PathBuf}; @@ -56,16 +57,6 @@ fn sweep( .and_then(Value::as_bool) .unwrap_or(false); - // Deploy-lanes is not ported. Python registers it whenever a lane is - // configured, so a sweep that quietly omitted it would answer differently - // from Python on every deployment that has one. - if !ctx.config.deploy_lanes.is_empty() { - return Err(VogtError::InvalidRequest( - "sweep cannot run while a deploy lane is configured: the deploy-lanes collector is not ported yet" - .to_string(), - )); - } - let projects = { let view = ctx.declared.read()?; if let Some(slug) = project_slug.as_deref() { @@ -122,8 +113,26 @@ fn sweep( .add(Registered::Indexer(Box::new(indexer))) .map_err(VogtError::InvalidRequest)?; } - if crate::adapters::forge::has_configured_forge(&ctx.config)? { - register_forge(&mut registry, ctx)?; + let forge = crate::adapters::forge::has_configured_forge(&ctx.config)?; + if forge || !ctx.config.deploy_lanes.is_empty() { + // One directory serves the forge reads and the lanes' receipts, opened + // once for the sweep, as Python's `forge_read_collectors` shares one. + let directory = Rc::new(crate::adapters::forge::ForgeClients::open( + ctx.config.clone(), + ctx.forge_transport(), + )?); + if forge { + register_forge(&mut registry, ctx, &directory)?; + } + if !ctx.config.deploy_lanes.is_empty() { + // Registered only when a lane is configured, so an instance without + // any reports deployed versions as "not configured", not "none". + registry + .add(Registered::Collector(Box::new(OwnedDeployLanes { + directory: Rc::clone(&directory), + }))) + .map_err(VogtError::InvalidRequest)?; + } } let mut selected: Vec = registry .select(&named, offline_only)? @@ -210,6 +219,22 @@ fn sweep( } drop(clock); + // After the feed has the sweep: a bound branch whose CI just settled is + // announced to its sessions. Contained, as Python contains it: a nudge that + // cannot be delivered must never fail a sweep whose evidence already landed. + if reports + .iter() + .any(|report| report.collector == crate::adapters::forge::COLLECTOR_CHECKS) + { + if let Err(error) = crate::application::ci_watch::announce_concluded(ctx) { + tracing::warn!( + target: "vogt.collect", + error = %format!("{}: {}", error.type_name(), error.message()), + "CI watch could not announce bound-branch conclusions" + ); + } + } + Ok(json!({ "scope": project_slug.unwrap_or_else(|| "all registered projects".to_string()), "projects": projects.len(), @@ -253,21 +278,18 @@ fn count_of(counts: &BTreeMap, key: &str) -> i64 { fn register_forge( registry: &mut CollectorRegistry, ctx: &AppContext, + directory: &Rc, ) -> Result<(), VogtError> { - let directory = Rc::new(crate::adapters::forge::ForgeClients::open( - ctx.config.clone(), - ctx.forge_transport(), - )?); let mut collectors = Vec::new(); for kind in [ForgeSyncKind::Issues, ForgeSyncKind::Pulls] { collectors.push(Registered::Collector(Box::new(OwnedForgeSync::new( kind, ctx.observed.clone(), - Rc::clone(&directory), + Rc::clone(directory), )))); } for kind in ForgeReadKind::ALL { - collectors.push(owned_forge_read(ctx.observed.clone(), &directory, kind)); + collectors.push(owned_forge_read(ctx.observed.clone(), directory, kind)); } for collector in collectors { registry.add(collector).map_err(VogtError::InvalidRequest)?; @@ -275,6 +297,37 @@ fn register_forge( Ok(()) } +/// The deploy-lanes collector as the sweep registry holds it: one observation +/// per configured lane, read through the sweep's shared directory and the real +/// version fetch. +pub(crate) struct OwnedDeployLanes { + directory: Rc, +} + +impl crate::collectors::Collector for OwnedDeployLanes { + fn name(&self) -> &str { + crate::adapters::forge::COLLECTOR_DEPLOY_LANES + } + + fn requires_network(&self) -> bool { + true + } + + fn collect( + &mut self, + ctx: &mut CollectorContext<'_>, + project: &crate::core::Project, + _now: &mut dyn FnMut() -> crate::core::Moment, + ) -> crate::collectors::Collection { + use crate::adapters::forge::{DeployLanesCollector, HttpVersionFetch}; + let directory: &dyn crate::adapters::forge::ForgeDirectory = &*self.directory; + let found: Result<_, VogtError> = + Ok(DeployLanesCollector::new(directory, &HttpVersionFetch) + .collect(ctx.config, project)); + found.into() + } +} + /// Which provider read an [`OwnedForgeRead`] performs. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum ForgeReadKind { @@ -1110,18 +1163,20 @@ mod tests { } } - /// A deploy lane still refuses the whole sweep, and the refusal names the - /// collector that is not ported yet. + /// A configured lane is swept with the rest: its collector runs over the + /// lane's project and reports one observation, with no receipt, no version + /// endpoint and no forge to read. #[test] - fn a_configured_deploy_lane_refuses_the_sweep() { + fn a_configured_deploy_lane_is_swept_and_reports_its_lane() { let dir = std::env::temp_dir().join(format!( - "vogt-collect-refuse-{}-{}", + "vogt-collect-lane-{}-{}", std::process::id(), unique() )); + let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let config = VogtConfig { - data_dir: dir, + data_dir: dir.clone(), deploy_lanes: vec![crate::config::DeployLane { name: "dev".to_string(), project: "alpha".to_string(), @@ -1132,21 +1187,27 @@ mod tests { }], ..VogtConfig::default() }; + let mut clock = Some(StepClock::new(moment())); + let mut ids = Some(SequentialIds::new(None).unwrap()); + crate::application::instance::init(&dir, &mut clock, &mut ids).unwrap(); let laned = build_context( config, Some(Principal::new("local:test-user", ActorKind::Human, "Test").unwrap()), - Some(StepClock::new(moment())), - Some(SequentialIds::new(None).unwrap()), + clock, + ids, None, None, None, None, ) .unwrap(); - let refused = sweep_op(&laned, json!({})).unwrap_err(); - let message = refused.to_string(); - assert!(matches!(refused, VogtError::InvalidRequest(_)), "{message}"); - assert!(message.contains("deploy-lanes"), "{message}"); + register(&laned, "alpha", &dir, None); + + let swept = sweep_op(&laned, json!({"collectors": ["deploy-lanes"]})).unwrap(); + let report = &swept["reports"].as_array().unwrap()[0]; + assert_eq!(report["collector"], "deploy-lanes", "{swept}"); + assert_eq!(report["outcome"], "ok", "{swept}"); + assert_eq!(report["new"], 1, "{swept}"); } /// Coverage lists the unported collectors as `never_run` while a forge or a diff --git a/engine/core/src/application/mod.rs b/engine/core/src/application/mod.rs index b7682ac39..7e7eae177 100644 --- a/engine/core/src/application/mod.rs +++ b/engine/core/src/application/mod.rs @@ -4,6 +4,7 @@ //! `instance` is the bootstrap use-case. pub mod brief; +pub mod ci_watch; pub mod collect; pub mod context; pub mod contracts; diff --git a/engine/core/src/core.rs b/engine/core/src/core.rs index 230513c7e..acc5af58a 100644 --- a/engine/core/src/core.rs +++ b/engine/core/src/core.rs @@ -266,7 +266,7 @@ pub fn py_repr(raw: &str) -> String { /// `str.isprintable`: a graphic character or a space, and the only space is /// U+0020. C0/C1 controls, format characters, line and paragraph separators, /// private-use and unassigned code points are all rejected. -fn py_printable(ch: char) -> bool { +pub(crate) fn py_printable(ch: char) -> bool { if ch.is_control() || (ch.is_whitespace() && ch != ' ') { return false; } From 0fbed1169516846e52aad84e5fc13a525b20728a Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 05:59:23 +0000 Subject: [PATCH 3/7] fix(core): the sweep result carries the workspace disk, as Python's does (WI-1071) Python's SweepResult has a disk field, null when no engine answers. The parity check found the Rust sweep omitting it. Co-Authored-By: Claude Haiku 5.5 Signed-off-by: sprooty --- engine/core/src/application/collect.rs | 1 + engine/core/src/application/instance.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/core/src/application/collect.rs b/engine/core/src/application/collect.rs index c2e46052e..8eb02224a 100644 --- a/engine/core/src/application/collect.rs +++ b/engine/core/src/application/collect.rs @@ -240,6 +240,7 @@ fn sweep( "projects": projects.len(), "subjects": subjects, "dep_refs": dep_refs, + "disk": crate::application::instance::workspace_disk(ctx).unwrap_or(Value::Null), "reports": reports.iter().map(report_view).collect::>(), })) } diff --git a/engine/core/src/application/instance.rs b/engine/core/src/application/instance.rs index 622d08dd7..1b860be36 100644 --- a/engine/core/src/application/instance.rs +++ b/engine/core/src/application/instance.rs @@ -436,7 +436,7 @@ fn status( /// The engine's report of the workspace volume, or nothing when it cannot be /// asked. A failure to answer is logged and read as absent, never as an error /// in status. -fn workspace_disk( +pub(crate) fn workspace_disk( ctx: &crate::application::context::AppContext, ) -> Option { let engine = ctx.engine.as_ref()?; From 2ff54c8edf35881f40501440a7049f0589766438 Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 05:59:23 +0000 Subject: [PATCH 4/7] test(parity): drive sweep and coverage on all three transports (WI-1071) Two sweeps (unnamed, then git-local alone) and a coverage read, after the bootstrap admin exists so the HTTP driver can authenticate. Goldens at 21c95f6 are re-recorded from the Python core, and the Rust core agrees on cli (99), http (49) and mcp (99) steps. Co-Authored-By: Claude Haiku 5.5 Signed-off-by: sprooty --- tests/parity/script.json | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/parity/script.json b/tests/parity/script.json index c1ccb0dfe..622110fea 100644 --- a/tests/parity/script.json +++ b/tests/parity/script.json @@ -839,6 +839,55 @@ "auth": "admin" } }, + { + "operation": "sweep", + "params": { + "reason": "parity harness" + }, + "expect": { + "exit": 0 + }, + "http": { + "method": "POST", + "path": "/api/sweep", + "auth": "admin", + "body": { + "reason": "parity harness" + } + } + }, + { + "operation": "sweep", + "params": { + "collectors": ["git-local"], + "reason": "parity harness" + }, + "expect": { + "exit": 0 + }, + "why": "a named sweep runs one collector and leaves the coverage of the rest as it was", + "http": { + "method": "POST", + "path": "/api/sweep", + "auth": "admin", + "body": { + "collectors": ["git-local"], + "reason": "parity harness" + } + } + }, + { + "operation": "coverage", + "params": {}, + "expect": { + "exit": 0 + }, + "http": { + "method": "GET", + "path": "/api/coverage", + "auth": "admin" + } + }, { "operation": "session.log_tail", "cli": [ From ec1f9354109c191e837c8be3649c9078e17af08c Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 06:52:06 +0000 Subject: [PATCH 5/7] test(parity): compare sweep findings and coverage; port test_ci_watch (WI-1071) Closes the three S6 review blockers on cd1546d2. - Parity compares what a sweep wrote. A `dump.observed` step dumps the observed store after the sweeps on all three transports. Coverage rows are no longer masked as volatile; only `last_swept_at` is blanked. The HTTP run registers projects (Second, Broken) so its sweep is not over zero projects. - A project whose .git cannot be read (Broken) is a parity case: git-local reports partial with the failing project named in coverage, not an empty success. - The normaliser blanks payload_digest for project registrations, which hash a payload carrying the run's temporary root. - `--only dump` runs the whole script and compares only the dump steps. - Rust observed store bumps latest_generation in rebuild_latest, as Python does. - test_ci_watch.py is fully ported: a test seam (ci_watch::seam) injects an announce failure, which the sweep contains (a_failed_announce_does_not_fail_the_sweep); the session nudge is tested (notice then Enter; none at a permission prompt). Golden tests/parity/golden/21c95f6 re-recorded from the Python core. Co-Authored-By: Claude Haiku 5.5 Signed-off-by: sprooty --- docs/PYTHON_TEST_DISPOSITION.md | 2 +- engine/core/src/application/ci_watch.rs | 147 ++++++++++++++++++++- engine/core/src/application/collect.rs | 57 ++++++++ engine/core/src/storage/sqlite/observed.rs | 9 ++ scripts/parity.py | 69 ++++++++-- tests/parity/normalise.toml | 25 +++- tests/parity/script.json | 47 ++++++- 7 files changed, 337 insertions(+), 19 deletions(-) diff --git a/docs/PYTHON_TEST_DISPOSITION.md b/docs/PYTHON_TEST_DISPOSITION.md index b85174b2c..3053e99ea 100644 --- a/docs/PYTHON_TEST_DISPOSITION.md +++ b/docs/PYTHON_TEST_DISPOSITION.md @@ -41,7 +41,7 @@ gain tests; it does not move down. | test_check_rust_version.py | retired | `scripts/check_rust_version.py`. A Python lint over the engine toolchain pin. | | test_check_workflow_policy.py | retired | `scripts/check_workflow_policy.py` over GitHub workflow YAML. CI policy, not product behaviour. | | test_checks.py | ported | CI-revision reads in the collectors and the project brief. | -| test_ci_watch.py | unported | Watched-ref and deployed-version reads are in `application/deployed.rs`. The bound-branch announce is in `application/ci_watch.rs`: the feed event once per conclusion, staleness, and the notice text. Still unported, owner WI-1071: `test_a_sweep_announces_and_survives_an_announce_failure`, which needs an announce failure injected into a sweep. The containment is in `application/collect.rs` and logs rather than failing. | +| test_ci_watch.py | ported | Watched-ref and deployed-version reads are in `application/deployed.rs`. The bound-branch announce is in `application/ci_watch.rs`: the feed event once per conclusion, staleness, the notice text, and the session nudge (notice, then Enter; none at a permission prompt). `test_a_sweep_announces_and_survives_an_announce_failure` is `a_failed_announce_does_not_fail_the_sweep` in `application/collect.rs`, driven by the `ci_watch::seam` failure switch. | | test_claude_profile.py | unported | Claude Code account profiles and their bootstrap. Named accounts land on `session.start` in WI-1306 (triage row 7782a7e8, under WI-1074). | | test_cli.py | ported | `adapters/cli.rs`. | | test_clone.py | ported | Clone service under `application/services`. | diff --git a/engine/core/src/application/ci_watch.rs b/engine/core/src/application/ci_watch.rs index 9c0badd54..a82c513db 100644 --- a/engine/core/src/application/ci_watch.rs +++ b/engine/core/src/application/ci_watch.rs @@ -41,6 +41,19 @@ const NOTICE_MAX_CHARS: usize = 400; /// The check observations read per project for the announce. Python's limit. const CHECKS_PER_PROJECT: i64 = 2000; +/// The announce's failure seam. A test sets `FAIL` and the next announce fails +/// the way an engine that fell over would, after counting the attempt in +/// `ATTEMPTS`. Thread-local, so each test owns its own switch. +#[cfg(test)] +pub(crate) mod seam { + use std::cell::Cell; + + thread_local! { + pub(crate) static FAIL: Cell = const { Cell::new(false) }; + pub(crate) static ATTEMPTS: Cell = const { Cell::new(0) }; + } +} + /// One branch a still-open work item is bound to, and its CI. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BoundBranch { @@ -227,6 +240,15 @@ fn fresh(concluded_at: Option<&str>, now_unix: i64) -> bool { pub fn announce_concluded( ctx: &AppContext, ) -> Result { + #[cfg(test)] + { + seam::ATTEMPTS.with(|attempts| attempts.set(attempts.get() + 1)); + if seam::FAIL.with(std::cell::Cell::get) { + return Err(VogtError::EngineUnavailable( + "announce failure injected by the test seam".to_owned(), + )); + } + } let found = { let view = ctx.declared.read()?; let mut projects: Vec = view @@ -342,7 +364,10 @@ mod tests { use crate::application::projects::register_op; use crate::application::services::work::{bind_branch_op, create_op}; use crate::config::VogtConfig; - use crate::core::{ActorKind, Moment, Principal, SequentialIds, StepClock, SweepOutcome}; + use crate::core::{ + ActorKind, CodingSession, Moment, Principal, SequentialIds, StepClock, SweepOutcome, + }; + use crate::storage::interface::WriteTxn; use crate::storage::observed_types::PendingObservation; use std::sync::{Arc, Mutex}; @@ -490,6 +515,71 @@ mod tests { ) } + /// A transport that answers every request as a session showing a + /// permission prompt does: the engine refuses input with `person required`. + fn prompting_engine() -> (Arc>>, EngineClient) { + let seen = Arc::new(Mutex::new(Vec::new())); + let record = Arc::clone(&seen); + let transport: Transport = Arc::new(move |url, _headers, body, method| { + record + .lock() + .unwrap() + .push(format!("{method} {url} {}", String::from_utf8_lossy(body))); + ( + 403, + br#"{"error":"forbidden: person required: a permission prompt"}"#.to_vec(), + ) + }); + ( + seen, + EngineClient::new("http://engine", Some("tok".to_string()), Some(transport)), + ) + } + + /// Start a live session for the item `WI-1` is in: a declared row with the + /// engine id the nudge types into. Written the way `start_session` writes + /// one, so the announce finds it through the item. + fn live_session(ctx: &AppContext, engine_id: &str) { + let (project_id, work_item_id, actor_id) = { + let view = ctx.declared.read().unwrap(); + let project_id = view + .list_projects(10, 0) + .unwrap() + .into_iter() + .next() + .unwrap() + .id; + let work_item_id = view + .work_item_by_ref("WI-1") + .unwrap() + .expect("WI-1 exists") + .id; + let actor_id = view + .actor_by_identity("local:test-user") + .unwrap() + .expect("the test principal is an actor") + .id; + (project_id, work_item_id, actor_id) + }; + let mut txn = ctx.declared.write().unwrap(); + txn.insert_session(&CodingSession { + id: format!("ses_{engine_id}"), + engine_session_id: engine_id.to_owned(), + project_id, + work_item_id: Some(work_item_id), + actor_id, + cwd: "/tmp/app".to_owned(), + template: None, + model: None, + effort: None, + reason: "test".to_owned(), + started_at: moment_now(ctx), + stopped_at: None, + }) + .unwrap(); + txn.commit().unwrap(); + } + fn settled_ci(state: BranchState, failing: &[&str]) -> BranchCi { BranchCi { branch: "wi-9".to_owned(), @@ -579,6 +669,61 @@ mod tests { assert_eq!(concluded_events(&built).len(), 1); } + #[test] + fn a_bound_session_is_nudged_once_with_the_notice_then_enter() { + let (seen, engine) = recording_engine(); + let built = bound_instance("nudge", Some(engine)); + let ctx = context(&built); + live_session(ctx, "eng-nudge"); + let at = stamp(ctx); + + seed( + ctx, + &[("ci", Some("failure")), ("lint", Some("success"))], + &at, + ); + assert_eq!(announce_concluded(ctx).unwrap(), 1); + + // The notice is typed, then the Enter that submits it, and nothing + // else: the person flag stays off, so the engine can refuse a prompt. + let sent = seen.lock().unwrap().clone(); + assert_eq!(sent.len(), 2, "{sent:?}"); + assert!(sent[0].starts_with("POST ") && sent[0].contains("/eng-nudge/input")); + assert!(sent[0].contains(r#""submit":false"#), "{}", sent[0]); + assert!(sent[0].contains("[vogt] CI FAILED on wi-1"), "{}", sent[0]); + assert!(sent[0].contains(r#""person":false"#), "{}", sent[0]); + assert!(sent[1].contains(r#""text":"""#), "{}", sent[1]); + assert!(sent[1].contains(r#""submit":true"#), "{}", sent[1]); + + let events = concluded_events(&built); + assert_eq!(events.len(), 1, "{events:?}"); + assert_eq!(events[0]["sessions_notified"], 1); + + // Once per conclusion: the next announce types nothing new. + assert_eq!(announce_concluded(ctx).unwrap(), 0); + assert_eq!(seen.lock().unwrap().len(), 2); + } + + #[test] + fn a_session_at_a_permission_prompt_gets_no_enter_and_the_announce_stands() { + let (seen, engine) = prompting_engine(); + let built = bound_instance("prompt", Some(engine)); + let ctx = context(&built); + live_session(ctx, "eng-prompt"); + let at = stamp(ctx); + + seed(ctx, &[("ci", Some("failure"))], &at); + assert_eq!(announce_concluded(ctx).unwrap(), 1); + + // The engine refused the notice, so the Enter after it was never sent. + let sent = seen.lock().unwrap().clone(); + assert_eq!(sent.len(), 1, "{sent:?}"); + assert!(sent[0].contains(r#""submit":false"#), "{}", sent[0]); + let events = concluded_events(&built); + assert_eq!(events.len(), 1, "{events:?}"); + assert_eq!(events[0]["sessions_notified"], 0); + } + #[test] fn a_stale_conclusion_is_never_announced() { let built = bound_instance("stale", None); diff --git a/engine/core/src/application/collect.rs b/engine/core/src/application/collect.rs index 8eb02224a..2e9fdd372 100644 --- a/engine/core/src/application/collect.rs +++ b/engine/core/src/application/collect.rs @@ -1164,6 +1164,63 @@ mod tests { } } + /// The announce runs after a checks collector, and its failure is contained: + /// the sweep reports the checks collector ok and does not fail, as Python's + /// does (`collect.py`). The seam makes the announce fail; the sweep is the + /// thing under test. + #[test] + fn a_failed_announce_does_not_fail_the_sweep() { + use crate::application::ci_watch::seam; + let dir = std::env::temp_dir().join(format!( + "vogt-collect-announce-{}-{}", + std::process::id(), + unique() + )); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let token = dir.join("forge-token"); + std::fs::write(&token, "secret-token\n").unwrap(); + let mut clock = Some(StepClock::new(moment())); + let mut ids = Some(SequentialIds::new(None).unwrap()); + crate::application::instance::init(&dir, &mut clock, &mut ids).unwrap(); + let forged = build_context( + VogtConfig { + data_dir: dir.clone(), + github_token_file: Some(token), + ..VogtConfig::default() + }, + Some(Principal::new("local:test-user", ActorKind::Human, "Test").unwrap()), + clock, + ids, + None, + None, + None, + None, + ) + .unwrap(); + // No repository, so forge-checks answers with its unsupported receipt + // and needs no network: the sweep is offline, and reports the checks. + register(&forged, "bare", &dir, None); + + seam::ATTEMPTS.with(|attempts| attempts.set(0)); + seam::FAIL.with(|fail| fail.set(true)); + let swept = sweep_op(&forged, json!({"collectors": ["forge-checks"]})); + seam::FAIL.with(|fail| fail.set(false)); + + let swept = swept.expect("a failed announce does not fail the sweep"); + assert_eq!( + seam::ATTEMPTS.with(std::cell::Cell::get), + 1, + "announced once" + ); + let reports = swept["reports"].as_array().unwrap(); + let checks = reports + .iter() + .find(|report| report["collector"] == "forge-checks") + .unwrap_or_else(|| panic!("forge-checks reported: {swept}")); + assert_eq!(checks["outcome"], "ok", "{checks}"); + } + /// A configured lane is swept with the rest: its collector runs over the /// lane's project and reports one observation, with no receipt, no version /// endpoint and no forge to read. diff --git a/engine/core/src/storage/sqlite/observed.rs b/engine/core/src/storage/sqlite/observed.rs index 65d6e04cb..b1aaf25f4 100644 --- a/engine/core/src/storage/sqlite/observed.rs +++ b/engine/core/src/storage/sqlite/observed.rs @@ -23,6 +23,10 @@ use crate::storage::sqlite::migrator::{self, migrations_root, table_exists}; const META_INSTANCE_ID: &str = "instance_id"; const META_CREATED_AT: &str = "created_at"; +/// Moves on every rebuild of `latest_observations`, as Python's +/// `META_LATEST_GENERATION` does: a cache keyed on it sees a new generation +/// exactly when the projection changed. +const META_LATEST_GENERATION: &str = "latest_generation"; const WITHHELD: &str = "[withheld: configuration or environment output]"; const OPEN_STATE_SQL: &str = "lower(coalesce(json_extract(payload, '$.state'), '')) NOT IN ('closed', 'merged')"; @@ -639,6 +643,11 @@ where fn rebuild_latest(&self) -> Result { let conn = self.open(true)?; let outcome = write_tx(&conn, || { + // Same transaction as the rebuild, as Python's `_bump_latest_generation`. + conn.execute( + "INSERT INTO meta (key, value) VALUES (?, '1') ON CONFLICT (key) DO UPDATE SET value = CAST(value AS INTEGER) + 1", + [META_LATEST_GENERATION], + )?; conn.execute("DELETE FROM latest_observations", [])?; conn.execute( "INSERT INTO latest_observations (subject_key, observation_id, collector, kind, project_id, payload, content_digest, source_url, promoted, observed_at) SELECT o.subject_key, o.id, o.collector, o.kind, o.project_id, o.payload, o.content_digest, o.source_url, o.promoted, o.observed_at FROM observations o WHERE o.id = (SELECT id FROM observations x WHERE x.subject_key = o.subject_key ORDER BY x.observed_at DESC, x.id DESC LIMIT 1)", diff --git a/scripts/parity.py b/scripts/parity.py index ca9619a09..30c1055bf 100644 --- a/scripts/parity.py +++ b/scripts/parity.py @@ -63,7 +63,10 @@ def main(argv: list[str] | None = None) -> int: return _selftest(args.golden) binary = _resolve(args.impl) - steps = _load_steps(args.only) + # `--only dump` runs the whole script and keeps only the dumps: a dump + # step reads what the sweeps wrote, and a script cut down to the dumps + # would dump an empty observed store. + steps = _load_steps("" if args.only == "dump" else args.only) if not steps and args.only != "dump": print(f"no steps match --only {args.only!r}", file=sys.stderr) return 2 @@ -87,6 +90,7 @@ def main(argv: list[str] | None = None) -> int: golden = json.loads((args.golden / f"{args.transport}.json").read_text()) if args.only == "dump": golden = [step for step in golden if step["operation"].startswith("dump.")] + recorded = [step for step in recorded if step["operation"].startswith("dump.")] elif args.only: golden = [step for step in golden if step["operation"].startswith(args.only)] # A CLI run always records dump.after_init first, even when the script @@ -126,15 +130,19 @@ def _load_steps(prefix: str) -> list[dict[str, Any]]: def _prepare_init(root: Path, env: dict[str, str]) -> dict[str, str]: - """Write the init pre-step's token files and return the env `init` runs with. + """Write the init pre-step's files and return the env `init` runs with. - The script's `init` block names bootstrap token files and the environment - that points at them. Every transport runs `init` with the same block, so - the adoption shows up in the after-init dump of each one. + The script's `init` block names the bootstrap token files, and any + fixture a step needs before it runs (a repository whose `.git` is broken, + for one). A name may sit in a subdirectory, which is created. Every + transport runs `init` with the same block, so the adoption shows up in the + after-init dump of each one. """ spec = json.loads(SCRIPT_PATH.read_text()).get("init", {}) for name, content in spec.get("files", {}).items(): - (root / name).write_text(content, encoding="utf-8") + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content.replace("{root}", str(root)), encoding="utf-8") init_env = dict(env) for key, value in spec.get("env", {}).items(): init_env[key] = value.replace("{root}", str(root)) @@ -165,7 +173,7 @@ def _run_mcp(binary: str, steps: list[dict[str, Any]]) -> list[dict[str, Any]]: recorded first, as the CLI run does. No final dump: the session token that the HTTP login mints is random, and nothing in this transport captures it. """ - steps = [step for step in steps if "params" in step] + steps = [step for step in steps if "params" in step or "dump" in step] with tempfile.TemporaryDirectory(prefix="vogt-parity-mcp-") as scratch: root = Path(scratch) data = root / "instance" @@ -235,6 +243,14 @@ def request(method: str, params: dict[str, Any] | None = None) -> Any: ) server.stdin.flush() for step in steps: + if "dump" in step: + recorded.append( + { + "operation": step["operation"], + "body": _dump(data, (step["dump"],)), + } + ) + continue arguments = _substitute(step["params"], {}, root) tool = step.get("mcp_tool", step["operation"].replace(".", "_")) reply = request("tools/call", {"name": tool, "arguments": arguments}) @@ -320,6 +336,15 @@ def _run_cli(binary: str, steps: list[dict[str, Any]]) -> list[dict[str, Any]]: } ) for step in steps: + if "dump" in step: + answers.append( + { + "operation": step["operation"], + "exit": 0, + "body": _dump(data, (step["dump"],)), + } + ) + continue if "params" not in step: continue # A step may give the CLI its own params where the CLI reads a @@ -367,8 +392,14 @@ def _run_cli(binary: str, steps: list[dict[str, Any]]) -> list[dict[str, Any]]: return answers -def _dump(data: Path) -> dict[str, Any]: - """Every non-empty table in both stores, after init. +def _dump( + data: Path, names: tuple[str, ...] = ("declared", "observed") +) -> dict[str, Any]: + """Every non-empty table in the named stores. + + Both stores after init by default. A dump step names the one it wants: + the observed store after a sweep holds the findings and the coverage rows + that the sweep wrote, and those are the evidence the sweep is judged on. The data directory is blanked because the run owns a temporary root. Everything else compares verbatim: the step clock fixes `applied_at` the @@ -378,7 +409,7 @@ def _dump(data: Path) -> dict[str, Any]: import sqlite3 stores: dict[str, Any] = {} - for name in ("declared", "observed"): + for name in names: path = data / f"{name}.sqlite3" conn = sqlite3.connect(path) conn.row_factory = sqlite3.Row @@ -441,7 +472,7 @@ def _run_http(binary: str, steps: list[dict[str, Any]]) -> list[dict[str, Any]]: import socket import urllib.request - steps = [step for step in steps if "http" in step] + steps = [step for step in steps if "http" in step or "dump" in step] recorded: list[dict[str, Any]] = [] with tempfile.TemporaryDirectory(prefix="vogt-parity-http-") as scratch: data = Path(scratch) / "instance" @@ -486,6 +517,18 @@ def _run_http(binary: str, steps: list[dict[str, Any]]) -> list[dict[str, Any]]: credentials: dict[str, str] = {} secrets: list[str] = [] for step in steps: + if "dump" in step: + # The store is read while the server runs: sqlite serves + # the committed rows, and nothing writes between steps. + recorded.append( + { + "operation": step["operation"], + "result": _normalise( + _dump(data, (step["dump"],)), Path(scratch), data + ), + } + ) + continue spec = step["http"] headers = dict(spec.get("headers") or {}) if "auth" in spec: @@ -634,6 +677,8 @@ def _walk( blank_version = top and operation in set(version.get("operations", [])) version_key = version.get("key") accepted = _product_versions() if blank_version else set() + digests = rules.get("digests", {}) + path_bearing = set(digests.get("path_bearing_operations", [])) walked = { key: ( "" @@ -645,6 +690,8 @@ def _walk( for key, item in value.items() if key not in drop } + if walked.get("operation") in path_bearing and "payload_digest" in walked: + walked["payload_digest"] = "" if ( in_schema and schema["sort_any_of"] diff --git a/tests/parity/normalise.toml b/tests/parity/normalise.toml index 522b13162..d030f79a1 100644 --- a/tests/parity/normalise.toml +++ b/tests/parity/normalise.toml @@ -16,16 +16,22 @@ # instance_id — minted by the id factory, which sequential ids fixes. # path — tokenised to / by the path rule below, so the remainder # is comparable. -# observed_at, last_swept_at, oldest_relevant_sweep, age_seconds — all read -# from ctx.clock, which the step clock fixes. +# observed_at, oldest_relevant_sweep, age_seconds — all read from ctx.clock, +# which the step clock fixes. +# +# Blanked: last_swept_at, the one timestamp a coverage row carries. Coverage +# rows are compared on their verdicts (status, detail, which projects) and +# counts. The clock reads the same on both cores, but when a sweep finished is +# not what coverage is asked about, so it stays out of the comparison. # # The HTTP run dumps the declared store. The server is started with # `--no-schedule`, so the background sweep never runs: it would consume # AUTOINCREMENT `events.seq` values between declared writes and race # `auth.logout` into a 500 (WI-1152). With it off, the declared tables — audit, # auth_decisions, tokens, events, actors, labels — are fixed by the hooks and -# compare verbatim. The observed store is not dumped: it holds the sweeps -# themselves, which drift the same way. +# compare verbatim. The observed store is dumped only where a script step asks +# for it (`dump = "observed"`), after the sweeps it is meant to show: with the +# scheduler off, its rows are the ones the steps caused. # # `token_hash` is deliberately not in this list. Python-issued tokens must # verify under Rust, and `core/auth.py` `hash_token` is a deterministic SHA-256, @@ -34,7 +40,7 @@ # a scheme divergence on the CLI too. why = "collector evidence names the machine; everything the hooks make deterministic compares verbatim" keys = [ - "collectors", "suppression", "evidence_snapshot", "evidence_observation_id", + "last_swept_at", "suppression", "evidence_snapshot", "evidence_observation_id", "secret", "data_dir", ] @@ -43,6 +49,15 @@ keys = [ why = "each run writes under its own temporary root" tokens = [{ needle_kind = "root", token = "" }, { needle_kind = "data", token = "" }] +[digests] +# A payload digest is a hash of the payload, and a project's payload carries +# its root path, which is the run's temporary root. Two runs never agree on +# that digest, so it is not evidence of anything. The project is compared on +# what the answer and the declared tables say about it, which is everything +# the payload held. +why = "a digest over a payload that carries the run's temporary root is not comparable across runs" +path_bearing_operations = ["project.register", "project.create", "project.import"] + [version] # The product version is stamped at build time, and only the answers that # report the build itself carry it. The rule substitutes only when diff --git a/tests/parity/script.json b/tests/parity/script.json index 622110fea..6d2c0fd56 100644 --- a/tests/parity/script.json +++ b/tests/parity/script.json @@ -6,7 +6,8 @@ "core-token": "vogt_bootstrap_core_secret_long_enough_0123", "agent-token": "vogt_agent_bootstrap_secret_long_enough_4567", "ada-password": "correct horse battery", - "ada-passphrase": "another passphrase" + "ada-passphrase": "another passphrase", + "broken/.git": "gitdir: {root}/nowhere\n" }, "env": { "VOGT_BOOTSTRAP_CORE_TOKEN_FILE": "{root}/core-token", @@ -815,6 +816,42 @@ "exit": 1 } }, + { + "operation": "project.register", + "http": { + "method": "POST", + "path": "/api/projects", + "auth": "admin", + "body": { + "name": "Second", + "root_path": "{root}/second", + "reason": "parity harness" + } + }, + "why": "HTTP has no project of its own: the same registration the CLI made for Second, here so the HTTP sweep has a project to sweep" + }, + { + "operation": "project.register", + "params": { + "name": "Broken", + "root_path": "{root}/broken", + "reason": "parity harness" + }, + "why": "a checkout whose .git cannot be read: git-local fails for this project, so its sweep is partial and coverage says so", + "expect": { + "exit": 0 + }, + "http": { + "method": "POST", + "path": "/api/projects", + "auth": "admin", + "body": { + "name": "Broken", + "root_path": "{root}/broken", + "reason": "parity harness" + } + } + }, { "operation": "session.list", "params": {}, @@ -888,6 +925,14 @@ "auth": "admin" } }, + { + "operation": "dump.observed", + "dump": "observed", + "why": "the findings and coverage rows the sweeps above wrote, after every sweep has run", + "expect": { + "exit": 0 + } + }, { "operation": "session.log_tail", "cli": [ From 302f28404b9cb382fd8540a8550bb914fcfb17c0 Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 07:29:24 +0000 Subject: [PATCH 6/7] fix(parity): HTTP sweep covers the registered repos; agent policy stored as Python stores it (WI-1071) - The parity repos Second and Broken live under the import root (/repos), so the remote-caller guard admits their HTTP registration. Broken's gitfile fixture moves with them, so git-local is still partial and the HTTP sweep covers both projects. - A project's agent_policy is stored as the model's compact JSON in field declaration order, as Python's model_dump_json writes it. The Rust AgentPolicy struct is reordered to match, which the HTTP dump now compares. - Golden tests/parity/golden/21c95f6 re-recorded from the Python core on top of origin/rust-core (cf2d3ca6). Co-Authored-By: Claude Haiku 5.5 Signed-off-by: sprooty --- engine/core/src/core.rs | 20 +- engine/core/src/storage/sqlite/declared.rs | 2 +- tests/parity/golden/21c95f6/http.json | 826 ++++++++++++++++++--- tests/parity/script.json | 10 +- 4 files changed, 744 insertions(+), 114 deletions(-) diff --git a/engine/core/src/core.rs b/engine/core/src/core.rs index acc5af58a..d93742109 100644 --- a/engine/core/src/core.rs +++ b/engine/core/src/core.rs @@ -1469,28 +1469,28 @@ impl Project { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct AgentPolicy { - #[serde(default)] - pub base_branch: String, - #[serde(default)] - pub commit_identity: String, - #[serde(default)] - pub deployed_setup: String, #[serde(default)] pub forbidden_text: Vec, #[serde(default)] - pub landing: Option, + pub merge_deploys_prod: bool, #[serde(default)] pub main_branches: Vec, #[serde(default)] - pub merge_deploys_prod: bool, + pub base_branch: String, #[serde(default)] - pub review: Option, + pub commit_identity: String, + #[serde(default)] + pub test_recipe: String, + #[serde(default)] + pub deployed_setup: String, #[serde(default)] pub rules: Vec, #[serde(default)] pub session_wall_clock_minutes: Option, #[serde(default)] - pub test_recipe: String, + pub landing: Option, + #[serde(default)] + pub review: Option, #[serde(default)] pub verification: Option, } diff --git a/engine/core/src/storage/sqlite/declared.rs b/engine/core/src/storage/sqlite/declared.rs index 45c2d6aed..8afac250a 100644 --- a/engine/core/src/storage/sqlite/declared.rs +++ b/engine/core/src/storage/sqlite/declared.rs @@ -1728,7 +1728,7 @@ impl WriteTxn for SqliteWrite { fn insert_project(&mut self, project: &Project) -> Result<(), VogtError> { self.view.conn.execute( "INSERT INTO projects (id, slug, name, root_path, repo_url, lifecycle_state, current_version, contract_version, compliance_status, compliance_checked_at, agent_policy, contract_adopted_at, write_back, link_state, exclusions, trust_state, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - params![project.id, project.slug, project.name, project.root_path, project.repo_url, project.lifecycle_state, project.current_version, project.contract_version, project.compliance_status, project.compliance_checked_at.map(to_iso), crate::decisions::python_json_dumps(&serde_json::json!(project.agent_policy), false), project.contract_adopted_at.map(to_iso), project.write_back, project.link_state, crate::decisions::python_json_dumps(&serde_json::json!(project.exclusions), false), project.trust_state, to_iso(project.created_at), to_iso(project.updated_at)], + params![project.id, project.slug, project.name, project.root_path, project.repo_url, project.lifecycle_state, project.current_version, project.contract_version, project.compliance_status, project.compliance_checked_at.map(to_iso), serde_json::to_string(&project.agent_policy).expect("an agent policy is plain data and serialises"), project.contract_adopted_at.map(to_iso), project.write_back, project.link_state, crate::decisions::python_json_dumps(&serde_json::json!(project.exclusions), false), project.trust_state, to_iso(project.created_at), to_iso(project.updated_at)], ).map_err(sql_err)?; Ok(()) } diff --git a/tests/parity/golden/21c95f6/http.json b/tests/parity/golden/21c95f6/http.json index f8b73a743..8a8dfbdf1 100644 --- a/tests/parity/golden/21c95f6/http.json +++ b/tests/parity/golden/21c95f6/http.json @@ -229,6 +229,100 @@ }, "status": 404 }, + { + "operation": "project.register", + "result": { + "project": { + "agent_policy": { + "base_branch": "", + "commit_identity": "", + "deployed_setup": "", + "forbidden_text": [], + "landing": null, + "main_branches": [], + "merge_deploys_prod": false, + "review": null, + "rules": [], + "session_wall_clock_minutes": null, + "test_recipe": "", + "verification": null + }, + "compliance_checked_at": null, + "compliance_status": "not_checked", + "contract_adopted_at": null, + "contract_version": null, + "created_at": "2026-01-02T03:04:06Z", + "current_version": null, + "exclusions": [ + ".venv/", + "node_modules/", + "target/", + "dist/", + "build/", + ".git/", + ".claude/" + ], + "id": "prj_0001", + "lifecycle_state": "active", + "link_state": "unlinked", + "name": "Second", + "repo_url": null, + "root_path": "/instance/repos/second", + "slug": "second", + "trust_state": "unverified", + "updated_at": "2026-01-02T03:04:06Z", + "write_back": "none" + } + }, + "status": 200 + }, + { + "operation": "project.register", + "result": { + "project": { + "agent_policy": { + "base_branch": "", + "commit_identity": "", + "deployed_setup": "", + "forbidden_text": [], + "landing": null, + "main_branches": [], + "merge_deploys_prod": false, + "review": null, + "rules": [], + "session_wall_clock_minutes": null, + "test_recipe": "", + "verification": null + }, + "compliance_checked_at": null, + "compliance_status": "not_checked", + "contract_adopted_at": null, + "contract_version": null, + "created_at": "2026-01-02T03:04:06Z", + "current_version": null, + "exclusions": [ + ".venv/", + "node_modules/", + "target/", + "dist/", + "build/", + ".git/", + ".claude/" + ], + "id": "prj_0002", + "lifecycle_state": "active", + "link_state": "unlinked", + "name": "Broken", + "repo_url": null, + "root_path": "/instance/repos/broken", + "slug": "broken", + "trust_state": "unverified", + "updated_at": "2026-01-02T03:04:06Z", + "write_back": "none" + } + }, + "status": 200 + }, { "operation": "session.list", "result": { @@ -254,6 +348,340 @@ }, "status": 200 }, + { + "operation": "sweep", + "result": { + "dep_refs": 0, + "disk": null, + "projects": 2, + "reports": [ + { + "collector": "dep-refs", + "failures": {}, + "new": 2, + "outcome": "ok", + "projects": 2, + "sweep_id": "swp_0001", + "unchanged": 0 + }, + { + "collector": "git-local", + "failures": { + "broken": "CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere" + }, + "new": 1, + "outcome": "partial", + "projects": 2, + "sweep_id": "swp_0002", + "unchanged": 0 + }, + { + "collector": "mirrored-source", + "failures": {}, + "new": 0, + "outcome": "ok", + "projects": 2, + "sweep_id": "swp_0003", + "unchanged": 0 + }, + { + "collector": "source-markers", + "failures": {}, + "new": 0, + "outcome": "ok", + "projects": 2, + "sweep_id": "swp_0004", + "unchanged": 0 + } + ], + "scope": "all registered projects", + "subjects": 3 + }, + "status": 200 + }, + { + "operation": "sweep", + "result": { + "dep_refs": 0, + "disk": null, + "projects": 2, + "reports": [ + { + "collector": "git-local", + "failures": { + "broken": "CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere" + }, + "new": 0, + "outcome": "partial", + "projects": 2, + "sweep_id": "swp_0005", + "unchanged": 1 + } + ], + "scope": "all registered projects", + "subjects": 3 + }, + "status": 200 + }, + { + "operation": "coverage", + "result": { + "collectors": [ + { + "age_seconds": null, + "collector": "contract-checker", + "detail": "this collector has not completed a sweep", + "last_sweep_scope": 0, + "last_swept_at": "", + "never_swept": 0, + "projects": 0, + "registered": 2, + "status": "never_run" + }, + { + "age_seconds": -2, + "collector": "dep-refs", + "detail": null, + "last_sweep_scope": 2, + "last_swept_at": "", + "never_swept": 0, + "projects": 2, + "registered": 2, + "status": "ok" + }, + { + "age_seconds": -5, + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "last_sweep_scope": 2, + "last_swept_at": "", + "never_swept": 0, + "projects": 2, + "registered": 2, + "status": "partial" + }, + { + "age_seconds": -8, + "collector": "mirrored-source", + "detail": null, + "last_sweep_scope": 2, + "last_swept_at": "", + "never_swept": 0, + "projects": 2, + "registered": 2, + "status": "ok" + }, + { + "age_seconds": -11, + "collector": "source-markers", + "detail": null, + "last_sweep_scope": 2, + "last_swept_at": "", + "never_swept": 0, + "projects": 2, + "registered": 2, + "status": "ok" + } + ], + "swept_project_ids": [ + "prj_0001", + "prj_0002" + ], + "unswept_project_ids": [] + }, + "status": 200 + }, + { + "operation": "dump.observed", + "result": { + "observed": { + "latest_observations": [ + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "kind": "dep_scan", + "observation_id": "obs_0001", + "observed_at": "2026-01-02T03:04:07+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:broken" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:efb8e7f9565a659b7236558520425790e45b29c9c53cc6687bad8156d87a0025", + "kind": "dep_scan", + "observation_id": "obs_0002", + "observed_at": "2026-01-02T03:04:07+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": false, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:second" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "kind": "git.checkout", + "observation_id": "obs_0003", + "observed_at": "2026-01-02T03:04:10+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "git:second" + } + ], + "meta": [ + { + "key": "instance_id", + "value": "ins_0001" + }, + { + "key": "created_at", + "value": "2026-01-02T03:04:08+00:00" + }, + { + "key": "latest_generation", + "value": "1" + } + ], + "migration_lock": [ + { + "acquired_at": null, + "holder": null, + "id": 1 + } + ], + "migrations": [ + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "c050f9e4dba983119f045b8fb4ccbe8b1ee97473170eead7d28279e99468de4b", + "id": "0001_foundation" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "9cdc9c9179740af96606bab0b557c7b35639eaabc78faf16630c7705f1d368f5", + "id": "0002_evidence" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "8bf0b8533774a156762fddc2e56bbb9656ba3b6bf0e38ec85a66c9dc9c325ed1", + "id": "0003_inherited_dep_refs" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "8d5cffbb2acb6ff18099ad73384eaa4ea5ebca7a6f45afb79d353fb4cbbcaee6", + "id": "0004_forge_sync" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "e6902e6616ab9948f9d222b2763ce9bc421a136b6acb1422240e5957683e5e45", + "id": "0005_perf_indexes" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "e0e959f4bf4f39db47d5e41ac11391d5f80a0b73bcce57d36168572062b1ab66", + "id": "0006_agent_activity" + } + ], + "observations": [ + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "id": "obs_0001", + "kind": "dep_scan", + "observed_at": "2026-01-02T03:04:07+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:broken", + "sweep_id": "swp_0001" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:efb8e7f9565a659b7236558520425790e45b29c9c53cc6687bad8156d87a0025", + "id": "obs_0002", + "kind": "dep_scan", + "observed_at": "2026-01-02T03:04:07+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": false, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:second", + "sweep_id": "swp_0001" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "id": "obs_0003", + "kind": "git.checkout", + "observed_at": "2026-01-02T03:04:10+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "git:second", + "sweep_id": "swp_0002" + } + ], + "sweeps": [ + { + "collector": "dep-refs", + "detail": null, + "finished_at": "2026-01-02T03:04:08+00:00", + "id": "swp_0001", + "outcome": "ok", + "scope": "[\"prj_0002\", \"prj_0001\"]", + "started_at": "2026-01-02T03:04:06+00:00", + "stats": "{\"projects\": 2, \"new\": 2, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "finished_at": "2026-01-02T03:04:11+00:00", + "id": "swp_0002", + "outcome": "partial", + "scope": "[\"prj_0002\", \"prj_0001\"]", + "started_at": "2026-01-02T03:04:09+00:00", + "stats": "{\"projects\": 2, \"new\": 1, \"unchanged\": 0, \"failed_projects\": 1}" + }, + { + "collector": "mirrored-source", + "detail": null, + "finished_at": "2026-01-02T03:04:14+00:00", + "id": "swp_0003", + "outcome": "ok", + "scope": "[\"prj_0002\", \"prj_0001\"]", + "started_at": "2026-01-02T03:04:12+00:00", + "stats": "{\"projects\": 2, \"new\": 0, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "source-markers", + "detail": null, + "finished_at": "2026-01-02T03:04:17+00:00", + "id": "swp_0004", + "outcome": "ok", + "scope": "[\"prj_0002\", \"prj_0001\"]", + "started_at": "2026-01-02T03:04:15+00:00", + "stats": "{\"projects\": 2, \"new\": 0, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "finished_at": "2026-01-02T03:04:08+00:00", + "id": "swp_0005", + "outcome": "partial", + "scope": "[\"prj_0002\", \"prj_0001\"]", + "started_at": "2026-01-02T03:04:06+00:00", + "stats": "{\"projects\": 2, \"new\": 0, \"unchanged\": 1, \"failed_projects\": 1}" + } + ] + } + } + }, { "operation": "session.log_tail", "result": { @@ -759,12 +1187,13 @@ "actor_id": "act_0004", "at": "2026-01-02T03:04:07Z", "audit_id": "aud_0006", - "entity_id": "lbl_0001", - "entity_kind": "label", - "kind": "label.created", + "entity_id": "prj_0001", + "entity_kind": "project", + "kind": "project.registered", "seq": 5, "summary": { - "name": "bug" + "name": "Second", + "slug": "second" } } ], @@ -822,12 +1251,12 @@ "at": "2026-01-02T03:04:07Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0013", + "id": "aud_0015", "operation": "user.remove", "payload_digest": "sha256:c641563a061b879b4b32211c6b39a7a2eeb71a7a114359133e079bc16f1b4bfa", "reason": "parity harness", - "revision": 11, - "txn_id": "txn_0013" + "revision": 13, + "txn_id": "txn_0020" }, { "actor_id": "act_0005", @@ -835,12 +1264,12 @@ "at": "2026-01-02T03:04:06Z", "entity_id": "tok_0005", "entity_kind": "token", - "id": "aud_0012", + "id": "aud_0014", "operation": "auth.login", "payload_digest": "sha256:079f75e3b73e507a9203394b2f453386de6362973c6d3e9dfa6ddfee0201f931", "reason": "password login by ada", - "revision": 10, - "txn_id": "txn_0012" + "revision": 12, + "txn_id": "txn_0019" }, { "actor_id": "act_0004", @@ -848,12 +1277,12 @@ "at": "2026-01-02T03:04:07Z", "entity_id": "tok_0002", "entity_kind": "token", - "id": "aud_0011", + "id": "aud_0013", "operation": "token.revoke", "payload_digest": "sha256:d331a6f9ffa337f5bbea02b91117f85c4a54fe0e8550538ef0ccbe9757a74c7a", "reason": "parity harness", - "revision": 9, - "txn_id": "txn_0011" + "revision": 11, + "txn_id": "txn_0018" }, { "actor_id": "act_0004", @@ -861,12 +1290,12 @@ "at": "2026-01-02T03:04:07Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0010", + "id": "aud_0012", "operation": "user.set_password", "payload_digest": "sha256:6782e2b5fa4d26772e8b2eeacbf82bb2c48960ddb25c736b28001d387114acf5", "reason": "parity harness", - "revision": 8, - "txn_id": "txn_0010" + "revision": 10, + "txn_id": "txn_0017" }, { "actor_id": "act_0004", @@ -874,15 +1303,15 @@ "at": "2026-01-02T03:04:07Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0009", + "id": "aud_0011", "operation": "user.create", "payload_digest": "sha256:67e02676f1f6d286e8b3624b34c5496ed548e0ac92c3485f85c350f4c54dcce7", "reason": "parity harness", - "revision": 7, - "txn_id": "txn_0009" + "revision": 9, + "txn_id": "txn_0016" } ], - "total": 13 + "total": 15 }, "status": 200 }, @@ -896,12 +1325,12 @@ "at": "2026-01-02T03:04:06Z", "entity_id": "tok_0005", "entity_kind": "token", - "id": "aud_0012", + "id": "aud_0014", "operation": "auth.login", "payload_digest": "sha256:079f75e3b73e507a9203394b2f453386de6362973c6d3e9dfa6ddfee0201f931", "reason": "password login by ada", - "revision": 10, - "txn_id": "txn_0012" + "revision": 12, + "txn_id": "txn_0019" }, { "actor_id": "act_0004", @@ -909,15 +1338,15 @@ "at": "2026-01-02T03:04:07Z", "entity_id": "tok_0002", "entity_kind": "token", - "id": "aud_0011", + "id": "aud_0013", "operation": "token.revoke", "payload_digest": "sha256:d331a6f9ffa337f5bbea02b91117f85c4a54fe0e8550538ef0ccbe9757a74c7a", "reason": "parity harness", - "revision": 9, - "txn_id": "txn_0011" + "revision": 11, + "txn_id": "txn_0018" } ], - "total": 13 + "total": 15 }, "status": 200 }, @@ -1047,101 +1476,125 @@ "revision": 3, "txn_id": "txn_0004" }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:07+00:00", + "entity_id": "prj_0001", + "entity_kind": "project", + "id": "aud_0006", + "operation": "project.register", + "payload_digest": "", + "reason": "parity harness", + "revision": 4, + "txn_id": "txn_0006" + }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:07+00:00", + "entity_id": "prj_0002", + "entity_kind": "project", + "id": "aud_0007", + "operation": "project.register", + "payload_digest": "", + "reason": "parity harness", + "revision": 5, + "txn_id": "txn_0007" + }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", "entity_id": "lbl_0001", "entity_kind": "label", - "id": "aud_0006", + "id": "aud_0008", "operation": "label.create", "payload_digest": "sha256:1b0a972c9944e759355152203a12fb36104f692b1f1911217107dcd4f75b8ccd", "reason": "seed", - "revision": 4, - "txn_id": "txn_0006" + "revision": 6, + "txn_id": "txn_0013" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", "entity_id": "tok_0004", "entity_kind": "token", - "id": "aud_0007", + "id": "aud_0009", "operation": "token.issue", "payload_digest": "sha256:1efafbf3c435f850162b7fdb5f1a1f97b97eb7bae0490f291c1731c7a390ee5a", "reason": "a read token", - "revision": 5, - "txn_id": "txn_0007" + "revision": 7, + "txn_id": "txn_0014" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", "entity_id": "tok_0004", "entity_kind": "token", - "id": "aud_0008", + "id": "aud_0010", "operation": "auth.logout", "payload_digest": "sha256:77f2ce31482b0061e1a20b86d2de25c0b43a6eaff6c025be826208b0dec15fe3", "reason": "done", - "revision": 6, - "txn_id": "txn_0008" + "revision": 8, + "txn_id": "txn_0015" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0009", + "id": "aud_0011", "operation": "user.create", "payload_digest": "sha256:67e02676f1f6d286e8b3624b34c5496ed548e0ac92c3485f85c350f4c54dcce7", "reason": "parity harness", - "revision": 7, - "txn_id": "txn_0009" + "revision": 9, + "txn_id": "txn_0016" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0010", + "id": "aud_0012", "operation": "user.set_password", "payload_digest": "sha256:6782e2b5fa4d26772e8b2eeacbf82bb2c48960ddb25c736b28001d387114acf5", "reason": "parity harness", - "revision": 8, - "txn_id": "txn_0010" + "revision": 10, + "txn_id": "txn_0017" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", "entity_id": "tok_0002", "entity_kind": "token", - "id": "aud_0011", + "id": "aud_0013", "operation": "token.revoke", "payload_digest": "sha256:d331a6f9ffa337f5bbea02b91117f85c4a54fe0e8550538ef0ccbe9757a74c7a", "reason": "parity harness", - "revision": 9, - "txn_id": "txn_0011" + "revision": 11, + "txn_id": "txn_0018" }, { "actor_id": "act_0005", "at": "2026-01-02T03:04:06+00:00", "entity_id": "tok_0005", "entity_kind": "token", - "id": "aud_0012", + "id": "aud_0014", "operation": "auth.login", "payload_digest": "sha256:079f75e3b73e507a9203394b2f453386de6362973c6d3e9dfa6ddfee0201f931", "reason": "password login by ada", - "revision": 10, - "txn_id": "txn_0012" + "revision": 12, + "txn_id": "txn_0019" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0013", + "id": "aud_0015", "operation": "user.remove", "payload_digest": "sha256:c641563a061b879b4b32211c6b39a7a2eeb71a7a114359133e079bc16f1b4bfa", "reason": "parity harness", - "revision": 11, - "txn_id": "txn_0013" + "revision": 13, + "txn_id": "txn_0020" } ], "auth_decisions": [ @@ -1256,6 +1709,32 @@ "detail": null, "id": "aut_0009", "identity_ref": "human:ada-lovelace", + "operation": "project.register", + "reason_code": "token_valid", + "scope": "project.write", + "token_id": "tok_0003", + "transport": "http" + }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:05+00:00", + "decision": "allow", + "detail": null, + "id": "aut_0010", + "identity_ref": "human:ada-lovelace", + "operation": "project.register", + "reason_code": "token_valid", + "scope": "project.write", + "token_id": "tok_0003", + "transport": "http" + }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:05+00:00", + "decision": "allow", + "detail": null, + "id": "aut_0011", + "identity_ref": "human:ada-lovelace", "operation": "session.list", "reason_code": "token_valid", "scope": "read", @@ -1267,7 +1746,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0010", + "id": "aut_0012", "identity_ref": "human:ada-lovelace", "operation": "session.sweep", "reason_code": "token_valid", @@ -1280,7 +1759,46 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0011", + "id": "aut_0013", + "identity_ref": "human:ada-lovelace", + "operation": "sweep", + "reason_code": "token_valid", + "scope": "project.write", + "token_id": "tok_0003", + "transport": "http" + }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:05+00:00", + "decision": "allow", + "detail": null, + "id": "aut_0014", + "identity_ref": "human:ada-lovelace", + "operation": "sweep", + "reason_code": "token_valid", + "scope": "project.write", + "token_id": "tok_0003", + "transport": "http" + }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:05+00:00", + "decision": "allow", + "detail": null, + "id": "aut_0015", + "identity_ref": "human:ada-lovelace", + "operation": "coverage", + "reason_code": "token_valid", + "scope": "read", + "token_id": "tok_0003", + "transport": "http" + }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:05+00:00", + "decision": "allow", + "detail": null, + "id": "aut_0016", "identity_ref": "human:ada-lovelace", "operation": "session.log_tail", "reason_code": "token_valid", @@ -1293,7 +1811,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0012", + "id": "aut_0017", "identity_ref": "human:ada-lovelace", "operation": "session.start", "reason_code": "token_valid", @@ -1306,7 +1824,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0013", + "id": "aut_0018", "identity_ref": "human:ada-lovelace", "operation": "session.input", "reason_code": "token_valid", @@ -1319,7 +1837,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0014", + "id": "aut_0019", "identity_ref": "human:ada-lovelace", "operation": "session.screen", "reason_code": "token_valid", @@ -1332,7 +1850,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0015", + "id": "aut_0020", "identity_ref": "human:ada-lovelace", "operation": "session.wait", "reason_code": "token_valid", @@ -1345,7 +1863,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0016", + "id": "aut_0021", "identity_ref": "human:ada-lovelace", "operation": "session.answer", "reason_code": "token_valid", @@ -1358,7 +1876,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0017", + "id": "aut_0022", "identity_ref": "human:ada-lovelace", "operation": "session.hibernate", "reason_code": "token_valid", @@ -1371,7 +1889,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0018", + "id": "aut_0023", "identity_ref": "human:ada-lovelace", "operation": "session.keep_awake", "reason_code": "token_valid", @@ -1384,7 +1902,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0019", + "id": "aut_0024", "identity_ref": "human:ada-lovelace", "operation": "session.set_role", "reason_code": "token_valid", @@ -1397,7 +1915,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0020", + "id": "aut_0025", "identity_ref": "human:ada-lovelace", "operation": "session.wake", "reason_code": "token_valid", @@ -1410,7 +1928,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0021", + "id": "aut_0026", "identity_ref": "human:ada-lovelace", "operation": "session.stop", "reason_code": "token_valid", @@ -1423,7 +1941,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0022", + "id": "aut_0027", "identity_ref": "human:ada-lovelace", "operation": "label.create", "reason_code": "token_valid", @@ -1436,7 +1954,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0023", + "id": "aut_0028", "identity_ref": "human:ada-lovelace", "operation": "token.issue", "reason_code": "token_valid", @@ -1449,7 +1967,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "deny", "detail": null, - "id": "aut_0024", + "id": "aut_0029", "identity_ref": "human:ada-lovelace", "operation": "label.create", "reason_code": "missing_scope", @@ -1462,7 +1980,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0025", + "id": "aut_0030", "identity_ref": "human:ada-lovelace", "operation": "auth.logout", "reason_code": "token_valid", @@ -1475,7 +1993,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0026", + "id": "aut_0031", "identity_ref": "human:ada-lovelace", "operation": "user.create", "reason_code": "token_valid", @@ -1488,7 +2006,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0027", + "id": "aut_0032", "identity_ref": "human:ada-lovelace", "operation": "user.list", "reason_code": "token_valid", @@ -1501,7 +2019,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0028", + "id": "aut_0033", "identity_ref": "human:ada-lovelace", "operation": "user.set_password", "reason_code": "token_valid", @@ -1514,7 +2032,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0029", + "id": "aut_0034", "identity_ref": "human:ada-lovelace", "operation": "token.list", "reason_code": "token_valid", @@ -1527,7 +2045,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0030", + "id": "aut_0035", "identity_ref": "human:ada-lovelace", "operation": "token.revoke", "reason_code": "token_valid", @@ -1540,7 +2058,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0031", + "id": "aut_0036", "identity_ref": "human:ada-lovelace", "operation": "auth.whoami", "reason_code": "token_valid", @@ -1553,7 +2071,7 @@ "at": "2026-01-02T03:04:07+00:00", "decision": "allow", "detail": null, - "id": "aut_0032", + "id": "aut_0037", "identity_ref": "human:ada", "operation": "auth.login", "reason_code": "login_ok", @@ -1566,7 +2084,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0033", + "id": "aut_0038", "identity_ref": "human:ada", "operation": "auth.whoami", "reason_code": "token_valid", @@ -1579,7 +2097,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0034", + "id": "aut_0039", "identity_ref": "human:ada-lovelace", "operation": "user.remove", "reason_code": "token_valid", @@ -1592,7 +2110,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0035", + "id": "aut_0040", "identity_ref": "human:ada-lovelace", "operation": "events.list", "reason_code": "token_valid", @@ -1605,7 +2123,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0036", + "id": "aut_0041", "identity_ref": "human:ada-lovelace", "operation": "events.list", "reason_code": "token_valid", @@ -1618,7 +2136,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0037", + "id": "aut_0042", "identity_ref": "human:ada-lovelace", "operation": "audit.list", "reason_code": "token_valid", @@ -1631,7 +2149,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0038", + "id": "aut_0043", "identity_ref": "human:ada-lovelace", "operation": "audit.list", "reason_code": "token_valid", @@ -1644,7 +2162,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0039", + "id": "aut_0044", "identity_ref": "human:ada-lovelace", "operation": "project.import", "reason_code": "token_valid", @@ -1657,7 +2175,7 @@ "at": "2026-01-02T03:04:05+00:00", "decision": "allow", "detail": null, - "id": "aut_0040", + "id": "aut_0045", "identity_ref": "human:ada-lovelace", "operation": "project.import", "reason_code": "token_valid", @@ -1711,80 +2229,150 @@ "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", "audit_id": "aud_0006", + "entity_id": "prj_0001", + "entity_kind": "project", + "kind": "project.registered", + "seq": 5, + "summary": "{\"slug\": \"second\", \"name\": \"Second\"}" + }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:07+00:00", + "audit_id": "aud_0007", + "entity_id": "prj_0002", + "entity_kind": "project", + "kind": "project.registered", + "seq": 6, + "summary": "{\"slug\": \"broken\", \"name\": \"Broken\"}" + }, + { + "actor_id": null, + "at": "2026-01-02T03:04:18+00:00", + "audit_id": null, + "entity_id": "swp_0001", + "entity_kind": "sweep", + "kind": "sweep.completed", + "seq": 7, + "summary": "{\"collector\": \"dep-refs\", \"outcome\": \"ok\", \"projects\": 2, \"new\": 2, \"unchanged\": 0}" + }, + { + "actor_id": null, + "at": "2026-01-02T03:04:19+00:00", + "audit_id": null, + "entity_id": "swp_0002", + "entity_kind": "sweep", + "kind": "sweep.completed", + "seq": 8, + "summary": "{\"collector\": \"git-local\", \"outcome\": \"partial\", \"projects\": 2, \"new\": 1, \"unchanged\": 0}" + }, + { + "actor_id": null, + "at": "2026-01-02T03:04:20+00:00", + "audit_id": null, + "entity_id": "swp_0003", + "entity_kind": "sweep", + "kind": "sweep.completed", + "seq": 9, + "summary": "{\"collector\": \"mirrored-source\", \"outcome\": \"ok\", \"projects\": 2, \"new\": 0, \"unchanged\": 0}" + }, + { + "actor_id": null, + "at": "2026-01-02T03:04:21+00:00", + "audit_id": null, + "entity_id": "swp_0004", + "entity_kind": "sweep", + "kind": "sweep.completed", + "seq": 10, + "summary": "{\"collector\": \"source-markers\", \"outcome\": \"ok\", \"projects\": 2, \"new\": 0, \"unchanged\": 0}" + }, + { + "actor_id": null, + "at": "2026-01-02T03:04:09+00:00", + "audit_id": null, + "entity_id": "swp_0005", + "entity_kind": "sweep", + "kind": "sweep.completed", + "seq": 11, + "summary": "{\"collector\": \"git-local\", \"outcome\": \"partial\", \"projects\": 2, \"new\": 0, \"unchanged\": 1}" + }, + { + "actor_id": "act_0004", + "at": "2026-01-02T03:04:07+00:00", + "audit_id": "aud_0008", "entity_id": "lbl_0001", "entity_kind": "label", "kind": "label.created", - "seq": 5, + "seq": 12, "summary": "{\"name\": \"bug\"}" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", - "audit_id": "aud_0007", + "audit_id": "aud_0009", "entity_id": "tok_0004", "entity_kind": "token", "kind": "token.issued", - "seq": 6, + "seq": 13, "summary": "{\"actor\": \"human:ada-lovelace\", \"scopes\": [\"read\"]}" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", - "audit_id": "aud_0008", + "audit_id": "aud_0010", "entity_id": "tok_0004", "entity_kind": "token", "kind": "auth.session_closed", - "seq": 7, + "seq": 14, "summary": "{\"actor\": \"human:ada-lovelace\"}" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", - "audit_id": "aud_0009", + "audit_id": "aud_0011", "entity_id": "act_0005", "entity_kind": "user", "kind": "user.created", - "seq": 8, + "seq": 15, "summary": "{\"actor\": \"human:ada\", \"username\": \"ada\"}" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", - "audit_id": "aud_0010", + "audit_id": "aud_0012", "entity_id": "act_0005", "entity_kind": "user", "kind": "user.password_set", - "seq": 9, + "seq": 16, "summary": "{\"username\": \"ada\"}" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", - "audit_id": "aud_0011", + "audit_id": "aud_0013", "entity_id": "tok_0002", "entity_kind": "token", "kind": "token.revoked", - "seq": 10, + "seq": 17, "summary": "{\"actor\": \"agent:vogt-sessions\"}" }, { "actor_id": "act_0005", "at": "2026-01-02T03:04:06+00:00", - "audit_id": "aud_0012", + "audit_id": "aud_0014", "entity_id": "tok_0005", "entity_kind": "token", "kind": "auth.session_opened", - "seq": 11, + "seq": 18, "summary": "{\"actor\": \"human:ada\"}" }, { "actor_id": "act_0004", "at": "2026-01-02T03:04:07+00:00", - "audit_id": "aud_0013", + "audit_id": "aud_0015", "entity_id": "act_0005", "entity_kind": "user", "kind": "user.removed", - "seq": 12, + "seq": 19, "summary": "{\"username\": \"ada\"}" } ], @@ -1810,7 +2398,7 @@ }, { "key": "revision", - "value": "11" + "value": "13" }, { "key": "work_ref_seq", @@ -1950,6 +2538,48 @@ "id": "0024_session_started_by" } ], + "projects": [ + { + "agent_policy": "{\"forbidden_text\":[],\"merge_deploys_prod\":false,\"main_branches\":[],\"base_branch\":\"\",\"commit_identity\":\"\",\"test_recipe\":\"\",\"deployed_setup\":\"\",\"rules\":[],\"session_wall_clock_minutes\":null,\"landing\":null,\"review\":null,\"verification\":null}", + "compliance_checked_at": null, + "compliance_status": "not_checked", + "contract_adopted_at": null, + "contract_version": null, + "created_at": "2026-01-02T03:04:06+00:00", + "current_version": null, + "exclusions": "[\".venv/\", \"node_modules/\", \"target/\", \"dist/\", \"build/\", \".git/\", \".claude/\"]", + "id": "prj_0001", + "lifecycle_state": "active", + "link_state": "unlinked", + "name": "Second", + "repo_url": null, + "root_path": "/instance/repos/second", + "slug": "second", + "trust_state": "unverified", + "updated_at": "2026-01-02T03:04:06+00:00", + "write_back": "none" + }, + { + "agent_policy": "{\"forbidden_text\":[],\"merge_deploys_prod\":false,\"main_branches\":[],\"base_branch\":\"\",\"commit_identity\":\"\",\"test_recipe\":\"\",\"deployed_setup\":\"\",\"rules\":[],\"session_wall_clock_minutes\":null,\"landing\":null,\"review\":null,\"verification\":null}", + "compliance_checked_at": null, + "compliance_status": "not_checked", + "contract_adopted_at": null, + "contract_version": null, + "created_at": "2026-01-02T03:04:06+00:00", + "current_version": null, + "exclusions": "[\".venv/\", \"node_modules/\", \"target/\", \"dist/\", \"build/\", \".git/\", \".claude/\"]", + "id": "prj_0002", + "lifecycle_state": "active", + "link_state": "unlinked", + "name": "Broken", + "repo_url": null, + "root_path": "/instance/repos/broken", + "slug": "broken", + "trust_state": "unverified", + "updated_at": "2026-01-02T03:04:06+00:00", + "write_back": "none" + } + ], "tokens": [ { "actor_id": "act_0002", diff --git a/tests/parity/script.json b/tests/parity/script.json index 6d2c0fd56..064c3bae0 100644 --- a/tests/parity/script.json +++ b/tests/parity/script.json @@ -7,7 +7,7 @@ "agent-token": "vogt_agent_bootstrap_secret_long_enough_4567", "ada-password": "correct horse battery", "ada-passphrase": "another passphrase", - "broken/.git": "gitdir: {root}/nowhere\n" + "instance/repos/broken/.git": "gitdir: {root}/nowhere\n" }, "env": { "VOGT_BOOTSTRAP_CORE_TOKEN_FILE": "{root}/core-token", @@ -182,7 +182,7 @@ "operation": "project.register", "params": { "name": "Second", - "root_path": "{root}/second", + "root_path": "{root}/instance/repos/second", "reason": "parity harness" }, "expect": { @@ -824,7 +824,7 @@ "auth": "admin", "body": { "name": "Second", - "root_path": "{root}/second", + "root_path": "{root}/instance/repos/second", "reason": "parity harness" } }, @@ -834,7 +834,7 @@ "operation": "project.register", "params": { "name": "Broken", - "root_path": "{root}/broken", + "root_path": "{root}/instance/repos/broken", "reason": "parity harness" }, "why": "a checkout whose .git cannot be read: git-local fails for this project, so its sweep is partial and coverage says so", @@ -847,7 +847,7 @@ "auth": "admin", "body": { "name": "Broken", - "root_path": "{root}/broken", + "root_path": "{root}/instance/repos/broken", "reason": "parity harness" } } From ab0d31e7d6850d160fba97c8830b1a0f694da331 Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 07:59:44 +0000 Subject: [PATCH 7/7] test(parity): re-record the 21c95f6 golden on rust-core (WI-1071) Co-Authored-By: Claude Opus 5.5 Signed-off-by: sprooty --- tests/parity/golden/21c95f6/cli.json | 511 +++++++++++++++++++++-- tests/parity/golden/21c95f6/mcp.json | 580 ++++++++++++++++++++++---- tests/parity/golden/21c95f6/meta.json | 2 +- 3 files changed, 971 insertions(+), 122 deletions(-) diff --git a/tests/parity/golden/21c95f6/cli.json b/tests/parity/golden/21c95f6/cli.json index d91a88145..eaccc7e5a 100644 --- a/tests/parity/golden/21c95f6/cli.json +++ b/tests/parity/golden/21c95f6/cli.json @@ -1139,7 +1139,7 @@ "link_state": "unlinked", "name": "Second", "repo_url": null, - "root_path": "/second", + "root_path": "/instance/repos/second", "slug": "second", "trust_state": "unverified", "updated_at": "2026-01-02T03:04:05Z", @@ -1251,7 +1251,7 @@ "detail": "nothing collected \u2014 either there is nothing to say, or the configured token cannot read notifications; `coverage` distinguishes the two", "freshness": { "age_seconds": null, - "collectors": "", + "collectors": {}, "detail": "no collector has completed a sweep yet", "oldest_relevant_sweep": null, "status": "never_swept" @@ -1709,7 +1709,7 @@ "excluded_unlinked": 0, "freshness": { "age_seconds": null, - "collectors": "", + "collectors": {}, "detail": "no collector has completed a sweep yet", "oldest_relevant_sweep": null, "status": "never_swept" @@ -1778,7 +1778,7 @@ "excluded_unlinked": 0, "freshness": { "age_seconds": null, - "collectors": "", + "collectors": {}, "detail": "no collector has completed a sweep yet", "oldest_relevant_sweep": null, "status": "never_swept" @@ -2276,6 +2276,53 @@ "exit": 1, "operation": "work.adopt" }, + { + "body": { + "project": { + "agent_policy": { + "base_branch": "", + "commit_identity": "", + "deployed_setup": "", + "forbidden_text": [], + "landing": null, + "main_branches": [], + "merge_deploys_prod": false, + "review": null, + "rules": [], + "session_wall_clock_minutes": null, + "test_recipe": "", + "verification": null + }, + "compliance_checked_at": null, + "compliance_status": "not_checked", + "contract_adopted_at": null, + "contract_version": null, + "created_at": "2026-01-02T03:04:05Z", + "current_version": null, + "exclusions": [ + ".venv/", + "node_modules/", + "target/", + "dist/", + "build/", + ".git/", + ".claude/" + ], + "id": "prj_0003", + "lifecycle_state": "active", + "link_state": "unlinked", + "name": "Broken", + "repo_url": null, + "root_path": "/instance/repos/broken", + "slug": "broken", + "trust_state": "unverified", + "updated_at": "2026-01-02T03:04:05Z", + "write_back": "none" + } + }, + "exit": 0, + "operation": "project.register" + }, { "body": { "digests": [], @@ -2301,6 +2348,392 @@ "exit": 0, "operation": "session.sweep" }, + { + "body": { + "dep_refs": 0, + "disk": null, + "projects": 3, + "reports": [ + { + "collector": "dep-refs", + "failures": {}, + "new": 3, + "outcome": "ok", + "projects": 3, + "sweep_id": "swp_0001", + "unchanged": 0 + }, + { + "collector": "git-local", + "failures": { + "broken": "CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere" + }, + "new": 2, + "outcome": "partial", + "projects": 3, + "sweep_id": "swp_0002", + "unchanged": 0 + }, + { + "collector": "mirrored-source", + "failures": {}, + "new": 0, + "outcome": "ok", + "projects": 3, + "sweep_id": "swp_0003", + "unchanged": 0 + }, + { + "collector": "source-markers", + "failures": {}, + "new": 0, + "outcome": "ok", + "projects": 3, + "sweep_id": "swp_0004", + "unchanged": 0 + } + ], + "scope": "all registered projects", + "subjects": 5 + }, + "exit": 0, + "operation": "sweep" + }, + { + "body": { + "dep_refs": 0, + "disk": null, + "projects": 3, + "reports": [ + { + "collector": "git-local", + "failures": { + "broken": "CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere" + }, + "new": 0, + "outcome": "partial", + "projects": 3, + "sweep_id": "swp_0005", + "unchanged": 2 + } + ], + "scope": "all registered projects", + "subjects": 5 + }, + "exit": 0, + "operation": "sweep" + }, + { + "body": { + "collectors": [ + { + "age_seconds": null, + "collector": "contract-checker", + "detail": "this collector has not completed a sweep", + "last_sweep_scope": 0, + "last_swept_at": "", + "never_swept": 0, + "projects": 0, + "registered": 3, + "status": "never_run" + }, + { + "age_seconds": -2, + "collector": "dep-refs", + "detail": null, + "last_sweep_scope": 3, + "last_swept_at": "", + "never_swept": 0, + "projects": 3, + "registered": 3, + "status": "ok" + }, + { + "age_seconds": -5, + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "last_sweep_scope": 3, + "last_swept_at": "", + "never_swept": 0, + "projects": 3, + "registered": 3, + "status": "partial" + }, + { + "age_seconds": -8, + "collector": "mirrored-source", + "detail": null, + "last_sweep_scope": 3, + "last_swept_at": "", + "never_swept": 0, + "projects": 3, + "registered": 3, + "status": "ok" + }, + { + "age_seconds": -11, + "collector": "source-markers", + "detail": null, + "last_sweep_scope": 3, + "last_swept_at": "", + "never_swept": 0, + "projects": 3, + "registered": 3, + "status": "ok" + } + ], + "swept_project_ids": [ + "prj_0001", + "prj_0002", + "prj_0003" + ], + "unswept_project_ids": [] + }, + "exit": 0, + "operation": "coverage" + }, + { + "body": { + "observed": { + "latest_observations": [ + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "kind": "dep_scan", + "observation_id": "obs_0001", + "observed_at": "2026-01-02T03:04:06+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0003", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:broken" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "kind": "dep_scan", + "observation_id": "obs_0002", + "observed_at": "2026-01-02T03:04:06+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:parity" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:efb8e7f9565a659b7236558520425790e45b29c9c53cc6687bad8156d87a0025", + "kind": "dep_scan", + "observation_id": "obs_0003", + "observed_at": "2026-01-02T03:04:06+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": false, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:second" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "kind": "git.checkout", + "observation_id": "obs_0004", + "observed_at": "2026-01-02T03:04:09+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "git:parity" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "kind": "git.checkout", + "observation_id": "obs_0005", + "observed_at": "2026-01-02T03:04:09+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "git:second" + } + ], + "meta": [ + { + "key": "instance_id", + "value": "ins_0001" + }, + { + "key": "created_at", + "value": "2026-01-02T03:04:08+00:00" + }, + { + "key": "latest_generation", + "value": "1" + } + ], + "migration_lock": [ + { + "acquired_at": null, + "holder": null, + "id": 1 + } + ], + "migrations": [ + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "c050f9e4dba983119f045b8fb4ccbe8b1ee97473170eead7d28279e99468de4b", + "id": "0001_foundation" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "9cdc9c9179740af96606bab0b557c7b35639eaabc78faf16630c7705f1d368f5", + "id": "0002_evidence" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "8bf0b8533774a156762fddc2e56bbb9656ba3b6bf0e38ec85a66c9dc9c325ed1", + "id": "0003_inherited_dep_refs" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "8d5cffbb2acb6ff18099ad73384eaa4ea5ebca7a6f45afb79d353fb4cbbcaee6", + "id": "0004_forge_sync" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "e6902e6616ab9948f9d222b2763ce9bc421a136b6acb1422240e5957683e5e45", + "id": "0005_perf_indexes" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "e0e959f4bf4f39db47d5e41ac11391d5f80a0b73bcce57d36168572062b1ab66", + "id": "0006_agent_activity" + } + ], + "observations": [ + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "id": "obs_0001", + "kind": "dep_scan", + "observed_at": "2026-01-02T03:04:06+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0003", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:broken", + "sweep_id": "swp_0001" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "id": "obs_0002", + "kind": "dep_scan", + "observed_at": "2026-01-02T03:04:06+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:parity", + "sweep_id": "swp_0001" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:efb8e7f9565a659b7236558520425790e45b29c9c53cc6687bad8156d87a0025", + "id": "obs_0003", + "kind": "dep_scan", + "observed_at": "2026-01-02T03:04:06+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": false, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:second", + "sweep_id": "swp_0001" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "id": "obs_0004", + "kind": "git.checkout", + "observed_at": "2026-01-02T03:04:09+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "git:parity", + "sweep_id": "swp_0002" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "id": "obs_0005", + "kind": "git.checkout", + "observed_at": "2026-01-02T03:04:09+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "git:second", + "sweep_id": "swp_0002" + } + ], + "sweeps": [ + { + "collector": "dep-refs", + "detail": null, + "finished_at": "2026-01-02T03:04:07+00:00", + "id": "swp_0001", + "outcome": "ok", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:04:05+00:00", + "stats": "{\"projects\": 3, \"new\": 3, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "finished_at": "2026-01-02T03:04:10+00:00", + "id": "swp_0002", + "outcome": "partial", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:04:08+00:00", + "stats": "{\"projects\": 3, \"new\": 2, \"unchanged\": 0, \"failed_projects\": 1}" + }, + { + "collector": "mirrored-source", + "detail": null, + "finished_at": "2026-01-02T03:04:13+00:00", + "id": "swp_0003", + "outcome": "ok", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:04:11+00:00", + "stats": "{\"projects\": 3, \"new\": 0, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "source-markers", + "detail": null, + "finished_at": "2026-01-02T03:04:16+00:00", + "id": "swp_0004", + "outcome": "ok", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:04:14+00:00", + "stats": "{\"projects\": 3, \"new\": 0, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "finished_at": "2026-01-02T03:04:07+00:00", + "id": "swp_0005", + "outcome": "partial", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:04:05+00:00", + "stats": "{\"projects\": 3, \"new\": 0, \"unchanged\": 2, \"failed_projects\": 1}" + } + ] + } + }, + "exit": 0, + "operation": "dump.observed" + }, { "body": { "bytes": 0, @@ -2681,12 +3114,12 @@ "at": "2026-01-02T03:04:06Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0027", + "id": "aud_0028", "operation": "user.remove", "payload_digest": "sha256:6cb1b570d1a756565af0191ec35d1fed1dc103ed9b82afa000b68a0172b6f711", "reason": "parity harness", - "revision": 26, - "txn_id": "txn_0030" + "revision": 27, + "txn_id": "txn_0036" }, { "actor_id": "act_0001", @@ -2694,12 +3127,12 @@ "at": "2026-01-02T03:04:06Z", "entity_id": "tok_0002", "entity_kind": "token", - "id": "aud_0026", + "id": "aud_0027", "operation": "token.revoke", "payload_digest": "sha256:5b5962b852bf72720c110fabfda9bea2589305bb33f47f25d1361ce815fa78c9", "reason": "parity harness", - "revision": 25, - "txn_id": "txn_0028" + "revision": 26, + "txn_id": "txn_0034" }, { "actor_id": "act_0001", @@ -2707,12 +3140,12 @@ "at": "2026-01-02T03:04:06Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0025", + "id": "aud_0026", "operation": "user.set_password", "payload_digest": "sha256:6782e2b5fa4d26772e8b2eeacbf82bb2c48960ddb25c736b28001d387114acf5", "reason": "parity harness", - "revision": 24, - "txn_id": "txn_0027" + "revision": 25, + "txn_id": "txn_0033" }, { "actor_id": "act_0001", @@ -2720,28 +3153,28 @@ "at": "2026-01-02T03:04:06Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0024", + "id": "aud_0025", "operation": "user.create", "payload_digest": "sha256:67e02676f1f6d286e8b3624b34c5496ed548e0ac92c3485f85c350f4c54dcce7", "reason": "parity harness", - "revision": 23, - "txn_id": "txn_0026" + "revision": 24, + "txn_id": "txn_0032" }, { "actor_id": "act_0001", "actor_identity_ref": "local:parity", - "at": "2026-01-02T03:04:05Z", - "entity_id": "prj_0001", + "at": "2026-01-02T03:04:06Z", + "entity_id": "prj_0003", "entity_kind": "project", - "id": "aud_0023", - "operation": "forge.onboard", - "payload_digest": "sha256:de96f57714a400d1aa3ccb1208c8d7f796cabd99d0fff7de7fdc85927be5ef34", + "id": "aud_0024", + "operation": "project.register", + "payload_digest": "", "reason": "parity harness", - "revision": 22, - "txn_id": "txn_0024" + "revision": 23, + "txn_id": "txn_0026" } ], - "total": 27 + "total": 28 }, "exit": 0, "operation": "audit.list" @@ -2755,12 +3188,12 @@ "at": "2026-01-02T03:04:06Z", "entity_id": "tok_0002", "entity_kind": "token", - "id": "aud_0026", + "id": "aud_0027", "operation": "token.revoke", "payload_digest": "sha256:5b5962b852bf72720c110fabfda9bea2589305bb33f47f25d1361ce815fa78c9", "reason": "parity harness", - "revision": 25, - "txn_id": "txn_0028" + "revision": 26, + "txn_id": "txn_0034" }, { "actor_id": "act_0001", @@ -2768,15 +3201,15 @@ "at": "2026-01-02T03:04:06Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0025", + "id": "aud_0026", "operation": "user.set_password", "payload_digest": "sha256:6782e2b5fa4d26772e8b2eeacbf82bb2c48960ddb25c736b28001d387114acf5", "reason": "parity harness", - "revision": 24, - "txn_id": "txn_0027" + "revision": 25, + "txn_id": "txn_0033" } ], - "total": 27 + "total": 28 }, "exit": 0, "operation": "audit.list" @@ -3010,7 +3443,7 @@ "detail": "no session engine is configured", "observed_at": null, "projects": 0, - "registered": 2, + "registered": 3, "source": "agent", "status": "unconfigured" }, @@ -3019,7 +3452,7 @@ "detail": "this collector has not completed a sweep", "observed_at": null, "projects": 0, - "registered": 2, + "registered": 3, "source": "ci", "status": "unswept" }, @@ -3028,7 +3461,7 @@ "detail": "open proposals in the declared store", "observed_at": null, "projects": 0, - "registered": 2, + "registered": 3, "source": "drift", "status": "current" }, @@ -3037,7 +3470,7 @@ "detail": "this collector has not completed a sweep", "observed_at": null, "projects": 0, - "registered": 2, + "registered": 3, "source": "github", "status": "unswept" } @@ -3256,7 +3689,7 @@ "detail": "no session engine is configured", "observed_at": null, "projects": 0, - "registered": 2, + "registered": 3, "source": "agent", "status": "unconfigured" }, @@ -3265,7 +3698,7 @@ "detail": "this collector has not completed a sweep", "observed_at": null, "projects": 0, - "registered": 2, + "registered": 3, "source": "ci", "status": "unswept" }, @@ -3274,7 +3707,7 @@ "detail": "open proposals in the declared store", "observed_at": null, "projects": 0, - "registered": 2, + "registered": 3, "source": "drift", "status": "current" }, @@ -3283,7 +3716,7 @@ "detail": "this collector has not completed a sweep", "observed_at": null, "projects": 0, - "registered": 2, + "registered": 3, "source": "github", "status": "unswept" } diff --git a/tests/parity/golden/21c95f6/mcp.json b/tests/parity/golden/21c95f6/mcp.json index 3f85f6895..5e18a5a51 100644 --- a/tests/parity/golden/21c95f6/mcp.json +++ b/tests/parity/golden/21c95f6/mcp.json @@ -1068,7 +1068,7 @@ "lifecycle_state": "active", "link_state": "unlinked", "name": "Second", - "root_path": "/second", + "root_path": "/instance/repos/second", "slug": "second", "trust_state": "unverified", "updated_at": "2026-01-02T03:04:18Z", @@ -1183,7 +1183,7 @@ "by_reason": {}, "detail": "nothing collected \u2014 either there is nothing to say, or the configured token cannot read notifications; `coverage` distinguishes the two", "freshness": { - "collectors": "", + "collectors": {}, "detail": "no collector has completed a sweep yet", "status": "never_swept" }, @@ -1475,7 +1475,7 @@ "declared": 1, "excluded_unlinked": 0, "freshness": { - "collectors": "", + "collectors": {}, "detail": "no collector has completed a sweep yet", "status": "never_swept" }, @@ -1515,7 +1515,7 @@ "declared": 0, "excluded_unlinked": 0, "freshness": { - "collectors": "", + "collectors": {}, "detail": "no collector has completed a sweep yet", "status": "never_swept" }, @@ -1997,6 +1997,44 @@ "is_error": true, "operation": "work.adopt" }, + { + "body": { + "project": { + "agent_policy": { + "base_branch": "", + "commit_identity": "", + "deployed_setup": "", + "forbidden_text": [], + "main_branches": [], + "merge_deploys_prod": false, + "rules": [], + "test_recipe": "" + }, + "compliance_status": "not_checked", + "created_at": "2026-01-02T03:04:55Z", + "exclusions": [ + ".venv/", + "node_modules/", + "target/", + "dist/", + "build/", + ".git/", + ".claude/" + ], + "id": "prj_0003", + "lifecycle_state": "active", + "link_state": "unlinked", + "name": "Broken", + "root_path": "/instance/repos/broken", + "slug": "broken", + "trust_state": "unverified", + "updated_at": "2026-01-02T03:04:55Z", + "write_back": "none" + } + }, + "is_error": false, + "operation": "project.register" + }, { "body": { "digests": [], @@ -2017,11 +2055,389 @@ }, "engine": "no session engine is configured (VOGT_ENGINE_URL is unset)", "rows": [], - "swept_at": "2026-01-02T03:04:56Z" + "swept_at": "2026-01-02T03:04:58Z" }, "is_error": false, "operation": "session.sweep" }, + { + "body": { + "dep_refs": 0, + "projects": 3, + "reports": [ + { + "collector": "dep-refs", + "failures": {}, + "new": 3, + "outcome": "ok", + "projects": 3, + "sweep_id": "swp_0001", + "unchanged": 0 + }, + { + "collector": "git-local", + "failures": { + "broken": "CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere" + }, + "new": 2, + "outcome": "partial", + "projects": 3, + "sweep_id": "swp_0002", + "unchanged": 0 + }, + { + "collector": "mirrored-source", + "failures": {}, + "new": 0, + "outcome": "ok", + "projects": 3, + "sweep_id": "swp_0003", + "unchanged": 0 + }, + { + "collector": "source-markers", + "failures": {}, + "new": 0, + "outcome": "ok", + "projects": 3, + "sweep_id": "swp_0004", + "unchanged": 0 + } + ], + "scope": "all registered projects", + "subjects": 5 + }, + "is_error": false, + "operation": "sweep" + }, + { + "body": { + "dep_refs": 0, + "projects": 3, + "reports": [ + { + "collector": "git-local", + "failures": { + "broken": "CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere" + }, + "new": 0, + "outcome": "partial", + "projects": 3, + "sweep_id": "swp_0005", + "unchanged": 2 + } + ], + "scope": "all registered projects", + "subjects": 5 + }, + "is_error": false, + "operation": "sweep" + }, + { + "body": { + "collectors": [ + { + "collector": "contract-checker", + "detail": "this collector has not completed a sweep", + "last_sweep_scope": 0, + "never_swept": 0, + "projects": 0, + "registered": 3, + "status": "never_run" + }, + { + "age_seconds": 18, + "collector": "dep-refs", + "last_sweep_scope": 3, + "last_swept_at": "", + "never_swept": 0, + "projects": 3, + "registered": 3, + "status": "ok" + }, + { + "age_seconds": 2, + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "last_sweep_scope": 3, + "last_swept_at": "", + "never_swept": 0, + "projects": 3, + "registered": 3, + "status": "partial" + }, + { + "age_seconds": 12, + "collector": "mirrored-source", + "last_sweep_scope": 3, + "last_swept_at": "", + "never_swept": 0, + "projects": 3, + "registered": 3, + "status": "ok" + }, + { + "age_seconds": 9, + "collector": "source-markers", + "last_sweep_scope": 3, + "last_swept_at": "", + "never_swept": 0, + "projects": 3, + "registered": 3, + "status": "ok" + } + ], + "swept_project_ids": [ + "prj_0001", + "prj_0002", + "prj_0003" + ], + "unswept_project_ids": [] + }, + "is_error": false, + "operation": "coverage" + }, + { + "body": { + "observed": { + "latest_observations": [ + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "kind": "dep_scan", + "observation_id": "obs_0001", + "observed_at": "2026-01-02T03:05:00+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0003", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:broken" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "kind": "dep_scan", + "observation_id": "obs_0002", + "observed_at": "2026-01-02T03:05:00+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:parity" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:efb8e7f9565a659b7236558520425790e45b29c9c53cc6687bad8156d87a0025", + "kind": "dep_scan", + "observation_id": "obs_0003", + "observed_at": "2026-01-02T03:05:00+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": false, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:second" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "kind": "git.checkout", + "observation_id": "obs_0004", + "observed_at": "2026-01-02T03:05:03+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "git:parity" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "kind": "git.checkout", + "observation_id": "obs_0005", + "observed_at": "2026-01-02T03:05:03+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "git:second" + } + ], + "meta": [ + { + "key": "instance_id", + "value": "ins_0001" + }, + { + "key": "created_at", + "value": "2026-01-02T03:04:08+00:00" + }, + { + "key": "latest_generation", + "value": "1" + } + ], + "migration_lock": [ + { + "acquired_at": null, + "holder": null, + "id": 1 + } + ], + "migrations": [ + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "c050f9e4dba983119f045b8fb4ccbe8b1ee97473170eead7d28279e99468de4b", + "id": "0001_foundation" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "9cdc9c9179740af96606bab0b557c7b35639eaabc78faf16630c7705f1d368f5", + "id": "0002_evidence" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "8bf0b8533774a156762fddc2e56bbb9656ba3b6bf0e38ec85a66c9dc9c325ed1", + "id": "0003_inherited_dep_refs" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "8d5cffbb2acb6ff18099ad73384eaa4ea5ebca7a6f45afb79d353fb4cbbcaee6", + "id": "0004_forge_sync" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "e6902e6616ab9948f9d222b2763ce9bc421a136b6acb1422240e5957683e5e45", + "id": "0005_perf_indexes" + }, + { + "applied_at": "2026-01-02T03:04:06+00:00", + "checksum": "e0e959f4bf4f39db47d5e41ac11391d5f80a0b73bcce57d36168572062b1ab66", + "id": "0006_agent_activity" + } + ], + "observations": [ + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "id": "obs_0001", + "kind": "dep_scan", + "observed_at": "2026-01-02T03:05:00+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0003", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:broken", + "sweep_id": "swp_0001" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:af09c76d426d6e99f4350729e7969584f83fc572511e128eedce52b46eeab94f", + "id": "obs_0002", + "kind": "dep_scan", + "observed_at": "2026-01-02T03:05:00+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": true, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:parity", + "sweep_id": "swp_0001" + }, + { + "collector": "dep-refs", + "content_digest": "sha256:efb8e7f9565a659b7236558520425790e45b29c9c53cc6687bad8156d87a0025", + "id": "obs_0003", + "kind": "dep_scan", + "observed_at": "2026-01-02T03:05:00+00:00", + "payload": "{\"manifests_read\": 0, \"references\": 0, \"root_exists\": false, \"unreadable_manifests\": [], \"unsupported_manifests\": []}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "depscan:second", + "sweep_id": "swp_0001" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "id": "obs_0004", + "kind": "git.checkout", + "observed_at": "2026-01-02T03:05:03+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0001", + "promoted": 0, + "source_url": null, + "subject_key": "git:parity", + "sweep_id": "swp_0002" + }, + { + "collector": "git-local", + "content_digest": "sha256:8dbc2923a29adb7ae1615b49a4ab4dc43d6bab026b898480c971647e4f376a4f", + "id": "obs_0005", + "kind": "git.checkout", + "observed_at": "2026-01-02T03:05:03+00:00", + "payload": "{\"detail\": \"no .git directory; this project is a plain folder\", \"is_git_repository\": false}", + "project_id": "prj_0002", + "promoted": 0, + "source_url": null, + "subject_key": "git:second", + "sweep_id": "swp_0002" + } + ], + "sweeps": [ + { + "collector": "dep-refs", + "detail": null, + "finished_at": "2026-01-02T03:05:01+00:00", + "id": "swp_0001", + "outcome": "ok", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:04:59+00:00", + "stats": "{\"projects\": 3, \"new\": 3, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "finished_at": "2026-01-02T03:05:04+00:00", + "id": "swp_0002", + "outcome": "partial", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:05:02+00:00", + "stats": "{\"projects\": 3, \"new\": 2, \"unchanged\": 0, \"failed_projects\": 1}" + }, + { + "collector": "mirrored-source", + "detail": null, + "finished_at": "2026-01-02T03:05:07+00:00", + "id": "swp_0003", + "outcome": "ok", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:05:05+00:00", + "stats": "{\"projects\": 3, \"new\": 0, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "source-markers", + "detail": null, + "finished_at": "2026-01-02T03:05:10+00:00", + "id": "swp_0004", + "outcome": "ok", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:05:08+00:00", + "stats": "{\"projects\": 3, \"new\": 0, \"unchanged\": 0, \"failed_projects\": 0}" + }, + { + "collector": "git-local", + "detail": "broken: CollectorError: git status could not be read in /instance/repos/broken: fatal: not a git repository: /nowhere", + "finished_at": "2026-01-02T03:05:17+00:00", + "id": "swp_0005", + "outcome": "partial", + "scope": "[\"prj_0003\", \"prj_0001\", \"prj_0002\"]", + "started_at": "2026-01-02T03:05:15+00:00", + "stats": "{\"projects\": 3, \"new\": 0, \"unchanged\": 2, \"failed_projects\": 1}" + } + ] + } + }, + "operation": "dump.observed" + }, { "body": { "bytes": 0, @@ -2122,13 +2538,13 @@ "user": { "actor_id": "act_0005", "actor_identity_ref": "human:ada", - "created_at": "2026-01-02T03:04:57Z", + "created_at": "2026-01-02T03:05:20Z", "scopes": [ "read", "work.write", "project.write" ], - "updated_at": "2026-01-02T03:04:57Z", + "updated_at": "2026-01-02T03:05:20Z", "username": "ada" } }, @@ -2141,13 +2557,13 @@ { "actor_id": "act_0005", "actor_identity_ref": "human:ada", - "created_at": "2026-01-02T03:04:57Z", + "created_at": "2026-01-02T03:05:20Z", "scopes": [ "read", "work.write", "project.write" ], - "updated_at": "2026-01-02T03:04:57Z", + "updated_at": "2026-01-02T03:05:20Z", "username": "ada" } ] @@ -2160,13 +2576,13 @@ "user": { "actor_id": "act_0005", "actor_identity_ref": "human:ada", - "created_at": "2026-01-02T03:04:57Z", + "created_at": "2026-01-02T03:05:20Z", "scopes": [ "read", "work.write", "project.write" ], - "updated_at": "2026-01-02T03:04:59Z", + "updated_at": "2026-01-02T03:05:22Z", "username": "ada" } }, @@ -2217,7 +2633,7 @@ "id": "tok_0002", "kind": "api", "name": "bootstrap agent token", - "revoked_at": "2026-01-02T03:05:01Z", + "revoked_at": "2026-01-02T03:05:24Z", "revoked_reason": "parity harness", "scopes": [ "read", @@ -2254,7 +2670,7 @@ "instance_id": "ins_0001", "observed_schema_version": 6, "path": "/backup", - "taken_at": "2026-01-02T03:05:03Z" + "taken_at": "2026-01-02T03:05:26Z" }, "is_error": false, "operation": "backup" @@ -2267,7 +2683,7 @@ "import_root_then": "/instance/repos", "instance_id": "ins_0001", "migrations_applied": [], - "restored_from": "2026-01-02T03:05:03Z", + "restored_from": "2026-01-02T03:05:26Z", "source": "/backup" }, "is_error": false, @@ -2409,70 +2825,70 @@ { "actor_id": "act_0001", "actor_identity_ref": "local:parity", - "at": "2026-01-02T03:05:08Z", + "at": "2026-01-02T03:05:31Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0024", + "id": "aud_0025", "operation": "user.remove", "payload_digest": "sha256:6cb1b570d1a756565af0191ec35d1fed1dc103ed9b82afa000b68a0172b6f711", "reason": "parity harness", - "revision": 23, - "txn_id": "txn_0029" + "revision": 24, + "txn_id": "txn_0035" }, { "actor_id": "act_0001", "actor_identity_ref": "local:parity", - "at": "2026-01-02T03:05:02Z", + "at": "2026-01-02T03:05:25Z", "entity_id": "tok_0002", "entity_kind": "token", - "id": "aud_0023", + "id": "aud_0024", "operation": "token.revoke", - "payload_digest": "sha256:f3a75264eaa1aa43d5f36af74de3d54874dcbc21dc80593ddb591d842696d099", + "payload_digest": "sha256:72df0d8c068af204558312866d5eb8de530834ebebaeb2a0a52f63f61eef79d6", "reason": "parity harness", - "revision": 22, - "txn_id": "txn_0027" + "revision": 23, + "txn_id": "txn_0033" }, { "actor_id": "act_0001", "actor_identity_ref": "local:parity", - "at": "2026-01-02T03:05:00Z", + "at": "2026-01-02T03:05:23Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0022", + "id": "aud_0023", "operation": "user.set_password", "payload_digest": "sha256:6782e2b5fa4d26772e8b2eeacbf82bb2c48960ddb25c736b28001d387114acf5", "reason": "parity harness", - "revision": 21, - "txn_id": "txn_0026" + "revision": 22, + "txn_id": "txn_0032" }, { "actor_id": "act_0001", "actor_identity_ref": "local:parity", - "at": "2026-01-02T03:04:58Z", + "at": "2026-01-02T03:05:21Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0021", + "id": "aud_0022", "operation": "user.create", "payload_digest": "sha256:67e02676f1f6d286e8b3624b34c5496ed548e0ac92c3485f85c350f4c54dcce7", "reason": "parity harness", - "revision": 20, - "txn_id": "txn_0025" + "revision": 21, + "txn_id": "txn_0031" }, { "actor_id": "act_0001", "actor_identity_ref": "local:parity", - "at": "2026-01-02T03:04:52Z", - "entity_id": "prj_0001", + "at": "2026-01-02T03:04:56Z", + "entity_id": "prj_0003", "entity_kind": "project", - "id": "aud_0020", - "operation": "forge.onboard", - "payload_digest": "sha256:de96f57714a400d1aa3ccb1208c8d7f796cabd99d0fff7de7fdc85927be5ef34", + "id": "aud_0021", + "operation": "project.register", + "payload_digest": "", "reason": "parity harness", - "revision": 19, - "txn_id": "txn_0023" + "revision": 20, + "txn_id": "txn_0025" } ], - "total": 24 + "total": 25 }, "is_error": false, "operation": "audit.list" @@ -2483,31 +2899,31 @@ { "actor_id": "act_0001", "actor_identity_ref": "local:parity", - "at": "2026-01-02T03:05:02Z", + "at": "2026-01-02T03:05:25Z", "entity_id": "tok_0002", "entity_kind": "token", - "id": "aud_0023", + "id": "aud_0024", "operation": "token.revoke", - "payload_digest": "sha256:f3a75264eaa1aa43d5f36af74de3d54874dcbc21dc80593ddb591d842696d099", + "payload_digest": "sha256:72df0d8c068af204558312866d5eb8de530834ebebaeb2a0a52f63f61eef79d6", "reason": "parity harness", - "revision": 22, - "txn_id": "txn_0027" + "revision": 23, + "txn_id": "txn_0033" }, { "actor_id": "act_0001", "actor_identity_ref": "local:parity", - "at": "2026-01-02T03:05:00Z", + "at": "2026-01-02T03:05:23Z", "entity_id": "act_0005", "entity_kind": "user", - "id": "aud_0022", + "id": "aud_0023", "operation": "user.set_password", "payload_digest": "sha256:6782e2b5fa4d26772e8b2eeacbf82bb2c48960ddb25c736b28001d387114acf5", "reason": "parity harness", - "revision": 21, - "txn_id": "txn_0026" + "revision": 22, + "txn_id": "txn_0032" } ], - "total": 24 + "total": 25 }, "is_error": false, "operation": "audit.list" @@ -2552,8 +2968,8 @@ "decision": { "asker": "local:parity", "context": "CI is green.", - "created_at": "2026-01-02T03:05:09Z", - "expires_at": "2026-01-03T03:05:09Z", + "created_at": "2026-01-02T03:05:32Z", + "expires_at": "2026-01-03T03:05:32Z", "id": "dec_0001", "options": [ { @@ -2580,8 +2996,8 @@ "decision": { "asker": "local:parity", "context": "", - "created_at": "2026-01-02T03:05:12Z", - "expires_at": "2026-01-03T03:05:12Z", + "created_at": "2026-01-02T03:05:35Z", + "expires_at": "2026-01-03T03:05:35Z", "id": "dec_0002", "options": [ { @@ -2609,8 +3025,8 @@ { "asker": "local:parity", "context": "", - "created_at": "2026-01-02T03:05:12Z", - "expires_at": "2026-01-03T03:05:12Z", + "created_at": "2026-01-02T03:05:35Z", + "expires_at": "2026-01-03T03:05:35Z", "id": "dec_0002", "options": [ { @@ -2631,8 +3047,8 @@ { "asker": "local:parity", "context": "CI is green.", - "created_at": "2026-01-02T03:05:09Z", - "expires_at": "2026-01-03T03:05:09Z", + "created_at": "2026-01-02T03:05:32Z", + "expires_at": "2026-01-03T03:05:32Z", "id": "dec_0001", "options": [ { @@ -2661,8 +3077,8 @@ { "asker": "local:parity", "context": "", - "created_at": "2026-01-02T03:05:12Z", - "expires_at": "2026-01-03T03:05:12Z", + "created_at": "2026-01-02T03:05:35Z", + "expires_at": "2026-01-03T03:05:35Z", "id": "dec_0002", "options": [ { @@ -2683,8 +3099,8 @@ { "asker": "local:parity", "context": "CI is green.", - "created_at": "2026-01-02T03:05:09Z", - "expires_at": "2026-01-03T03:05:09Z", + "created_at": "2026-01-02T03:05:32Z", + "expires_at": "2026-01-03T03:05:32Z", "id": "dec_0001", "options": [ { @@ -2720,7 +3136,7 @@ "count": 2, "detail": "no session engine is configured", "projects": 0, - "registered": 2, + "registered": 3, "source": "agent", "status": "unconfigured" }, @@ -2728,7 +3144,7 @@ "count": 0, "detail": "this collector has not completed a sweep", "projects": 0, - "registered": 2, + "registered": 3, "source": "ci", "status": "unswept" }, @@ -2736,7 +3152,7 @@ "count": 0, "detail": "open proposals in the declared store", "projects": 0, - "registered": 2, + "registered": 3, "source": "drift", "status": "current" }, @@ -2744,7 +3160,7 @@ "count": 0, "detail": "this collector has not completed a sweep", "projects": 0, - "registered": 2, + "registered": 3, "source": "github", "status": "unswept" } @@ -2765,7 +3181,7 @@ "evidence_snapshot": "", "freshness": "live", "kind": "decision.pending", - "occurred_at": "2026-01-02T03:05:12Z", + "occurred_at": "2026-01-02T03:05:35Z", "provisional": true, "source": "agent", "source_subject_key": "dec_0002", @@ -2786,7 +3202,7 @@ "evidence_snapshot": "", "freshness": "live", "kind": "decision.pending", - "occurred_at": "2026-01-02T03:05:09Z", + "occurred_at": "2026-01-02T03:05:32Z", "provisional": true, "source": "agent", "source_subject_key": "dec_0001", @@ -2798,10 +3214,10 @@ ], "github_scope": "registered projects only", "high_water": { - "agent": "2026-01-02T03:05:12Z" + "agent": "2026-01-02T03:05:35Z" }, "instance_scope": "registered projects only", - "snapshot_at": "2026-01-02T03:05:19Z" + "snapshot_at": "2026-01-02T03:05:42Z" }, "is_error": false, "operation": "inbox.list" @@ -2813,12 +3229,12 @@ "label": "Merge", "note": "reviewed" }, - "answered_at": "2026-01-02T03:05:25Z", + "answered_at": "2026-01-02T03:05:48Z", "answered_by": "local:parity", "asker": "local:parity", "context": "CI is green.", - "created_at": "2026-01-02T03:05:09Z", - "expires_at": "2026-01-03T03:05:09Z", + "created_at": "2026-01-02T03:05:32Z", + "expires_at": "2026-01-03T03:05:32Z", "id": "dec_0001", "options": [ { @@ -2859,8 +3275,8 @@ "decision": { "asker": "local:parity", "context": "", - "created_at": "2026-01-02T03:05:12Z", - "expires_at": "2026-01-03T03:05:12Z", + "created_at": "2026-01-02T03:05:35Z", + "expires_at": "2026-01-03T03:05:35Z", "id": "dec_0002", "options": [ { @@ -2890,12 +3306,12 @@ "label": "Merge", "note": "reviewed" }, - "answered_at": "2026-01-02T03:05:25Z", + "answered_at": "2026-01-02T03:05:48Z", "answered_by": "local:parity", "asker": "local:parity", "context": "CI is green.", - "created_at": "2026-01-02T03:05:09Z", - "expires_at": "2026-01-03T03:05:09Z", + "created_at": "2026-01-02T03:05:32Z", + "expires_at": "2026-01-03T03:05:32Z", "id": "dec_0001", "options": [ { @@ -2931,7 +3347,7 @@ "count": 0, "detail": "no session engine is configured", "projects": 0, - "registered": 2, + "registered": 3, "source": "agent", "status": "unconfigured" }, @@ -2939,7 +3355,7 @@ "count": 0, "detail": "this collector has not completed a sweep", "projects": 0, - "registered": 2, + "registered": 3, "source": "ci", "status": "unswept" }, @@ -2947,7 +3363,7 @@ "count": 0, "detail": "open proposals in the declared store", "projects": 0, - "registered": 2, + "registered": 3, "source": "drift", "status": "current" }, @@ -2955,7 +3371,7 @@ "count": 0, "detail": "this collector has not completed a sweep", "projects": 0, - "registered": 2, + "registered": 3, "source": "github", "status": "unswept" } @@ -2967,7 +3383,7 @@ "github_scope": "registered projects only", "high_water": {}, "instance_scope": "registered projects only", - "snapshot_at": "2026-01-02T03:05:33Z" + "snapshot_at": "2026-01-02T03:05:56Z" }, "is_error": false, "operation": "inbox.list" diff --git a/tests/parity/golden/21c95f6/meta.json b/tests/parity/golden/21c95f6/meta.json index 66055ee75..262922755 100644 --- a/tests/parity/golden/21c95f6/meta.json +++ b/tests/parity/golden/21c95f6/meta.json @@ -1,5 +1,5 @@ { "main_sha": "21c95f674e1b03ee177394f605abb1abfac41cdd", - "recorded_at": "2026-10-17T07:11:18Z", + "recorded_at": "2026-10-17T07:59:07Z", "recorder": "scripts/parity.py" }