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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ If code and docs disagree, stop and reconcile them instead of guessing.
- WorkGraph is not a generic agent runtime, generic workflow builder, generic task tracker, or generic memory layer.
- The context graph is first-class and typed. Wiki-links are one edge source, not the graph definition.
- The ledger is both audit trail and durable event stream.
- Triggers are core infrastructure, even when the current foundation pass only yields durable planned follow-up actions.
- Triggers are core infrastructure, even when the current phase only yields durable planned follow-up actions rather than live execution.
- Threads are evidence-bearing coordination units, not chat logs or loose tasks.
- Missions coordinate related work. Runs capture one execution instance. Triggers yield planned follow-up actions.
- The actor model must scale to hundreds or thousands of actors while allowing opaque subactor lineages.
Expand Down Expand Up @@ -82,12 +82,12 @@ The CLI (`wg-cli`) is the **default interface** for all agents with shell access
- `workgraph status` should expose graph hygiene and evidence gaps, not only counts.
- `workgraph show` should render coordination primitives in a way that makes their contracts obvious to humans and agents.

## Out Of Scope For This Foundation Pass
## Out Of Scope For This Phase

- live trigger execution loops
- webhook ingress runtime
- webhook ingress HTTP runtime
- remote MCP/API server implementation
- approval workflow execution
- ergonomic nested authoring flows beyond direct markdown editing

Those are later layers. The foundation pass exists to make those future layers disciplined rather than improvised.
Those are later layers. The current trigger-plane expansion exists to make those future layers disciplined rather than improvised.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ The current workspace encodes that durable foundation rather than only describin
- first-class thread, mission, run, trigger, checkpoint, and actor-lineage contracts in `wg-types`
- evidence-bearing thread persistence in `wg-thread`
- mission and run persistence in `wg-mission` and `wg-dispatch`, including mission planning/approval/validation states, milestone thread auto-creation, and run start/end timestamps
- typed graph edges in `wg-graph`, including assignment, containment, evidence, trigger, reference, and actor-lineage edges derived from agent metadata
- typed graph edges in `wg-graph`, including assignment, containment, evidence, trigger, reference, actor-lineage, and trigger-receipt edges derived from durable coordination state
- orientation and CLI surfaces that expose evidence gaps, graph issues, coordination contracts, and full primitive discovery metadata
- trigger evaluation over normalized ledger, internal, and webhook event envelopes with durable `trigger_receipt` primitives for replay-safe planned follow-up actions

This turn does not implement live trigger execution loops, webhook ingress, remote MCP, or API runtime surfaces yet. It establishes the durable contracts those surfaces must honor.
This turn does not implement live trigger execution loops, webhook HTTP runtime, remote MCP, or API runtime surfaces yet. It establishes the durable contracts those surfaces must honor.

CLI creation paths now evaluate persisted policy primitives before writing. Trigger action plans remain durable planned follow-up actions rather than auto-executed effects in this foundation pass.
CLI creation paths evaluate persisted policy primitives before writing. Trigger evaluation now records replay-safe `trigger_receipt` primitives and policy-aware planned action outcomes rather than auto-executed effects.

## Product Boundary

Expand Down
1 change: 1 addition & 0 deletions crates/wg-adapter-webhook/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ authors.workspace = true

[dependencies]
wg-adapter-api = { path = "../wg-adapter-api" }
wg-types = { path = "../wg-types" }
10 changes: 9 additions & 1 deletion crates/wg-adapter-webhook/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#![forbid(unsafe_code)]

//! HTTP webhook adapter placeholder.
//! Normalized webhook event ingress helpers for the trigger plane.

use wg_adapter_api::{AdapterRequest, AdapterStatus, RuntimeAdapter};
use wg_types::{EventEnvelope, EventSourceKind};

/// Placeholder adapter for webhook-triggered runs.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
Expand All @@ -17,6 +18,13 @@ impl WebhookAdapter {
pub const fn new() -> Self {
Self
}

/// Normalizes a provider webhook payload into a trigger-plane event envelope.
#[must_use]
pub fn normalize_event(self, event: EventEnvelope) -> EventEnvelope {
debug_assert_eq!(event.source, EventSourceKind::Webhook);
event
}
}

impl RuntimeAdapter for WebhookAdapter {
Expand Down
4 changes: 4 additions & 0 deletions crates/wg-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#![forbid(unsafe_code)]

//! API surface placeholders for HTTP, gRPC, SSE, and webhook endpoints.
//!
//! Phase 3 keeps event-plane semantics in the kernel and CLI. This crate remains
//! a transport-thin placeholder until remote runtime surfaces are intentionally
//! implemented in a later phase.

/// Transport exposed by the placeholder API server.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
Expand Down
2 changes: 2 additions & 0 deletions crates/wg-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ authors.workspace = true

[dependencies]
anyhow.workspace = true
chrono.workspace = true
clap.workspace = true
serde.workspace = true
serde_json.workspace = true
Expand All @@ -24,6 +25,7 @@ wg-paths = { path = "../wg-paths" }
wg-policy = { path = "../wg-policy" }
wg-registry = { path = "../wg-registry" }
wg-store = { path = "../wg-store" }
wg-trigger = { path = "../wg-trigger" }
wg-error = { path = "../wg-error" }
wg-types = { path = "../wg-types" }

Expand Down
65 changes: 65 additions & 0 deletions crates/wg-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ pub enum Command {
#[command(subcommand)]
command: RunCommand,
},
/// Manages trigger validation, replay, and event ingestion workflows.
#[command(
after_help = "Examples:\n workgraph trigger validate trigger/react-to-thread-complete\n workgraph trigger replay --last 20\n workgraph trigger ingest --source internal --event-name signal.sent --field subject_reference=thread/thread-1 --field actor_id=agent:cursor"
)]
Trigger {
/// Trigger-specific subcommand to execute.
#[command(subcommand)]
command: TriggerCommand,
},
}

impl Command {
Expand All @@ -193,6 +202,7 @@ impl Command {
Self::Query { .. } => "query",
Self::Show { .. } => "show",
Self::Run { command } => command.name(),
Self::Trigger { command } => command.name(),
}
}
}
Expand Down Expand Up @@ -293,6 +303,61 @@ impl RunCommand {
}
}

/// Supported `workgraph trigger` subcommands.
#[derive(Debug, Subcommand)]
pub enum TriggerCommand {
/// Validates a stored trigger definition by `<type>/<id>` reference.
#[command(
after_help = "Examples:\n workgraph trigger validate trigger/react-to-thread-complete\n workgraph --json trigger validate trigger/react-to-thread-complete"
)]
Validate {
/// Trigger reference in `<type>/<id>` form.
reference: String,
},
/// Replays recent ledger entries through the trigger plane.
#[command(
after_help = "Examples:\n workgraph trigger replay\n workgraph trigger replay --last 20\n workgraph --json trigger replay --last 5"
)]
Replay {
/// Number of most recent ledger entries to replay.
#[arg(long)]
last: Option<usize>,
},
/// Ingests one normalized event into the trigger plane without a live runtime.
#[command(
after_help = "Examples:\n workgraph trigger ingest --source internal --event-name signal.sent --field subject_reference=thread/thread-1\n workgraph --json trigger ingest --source webhook --provider github --event-name pull_request.merged --field subject_reference=project/dealer-portal"
)]
Ingest {
/// Event source kind.
#[arg(long)]
source: String,
/// Stable event name.
#[arg(long = "event-name")]
event_name: Option<String>,
/// Provider or emitter for webhook/internal events.
#[arg(long)]
provider: Option<String>,
/// Explicit event id. When omitted, a deterministic id is derived from fields.
#[arg(long = "event-id")]
event_id: Option<String>,
/// Event payload fields expressed as `key=value`.
#[arg(long = "field", value_parser = parse_key_value_input)]
fields: Vec<KeyValueInput>,
},
}

impl TriggerCommand {
/// Returns the stable command name associated with this parsed trigger subcommand.
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Validate { .. } => "trigger_validate",
Self::Replay { .. } => "trigger_replay",
Self::Ingest { .. } => "trigger_ingest",
}
}
}

/// A parsed `key=value` argument pair used by create and query commands.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyValueInput {
Expand Down
4 changes: 4 additions & 0 deletions crates/wg-cli/src/commands/brief.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ fn build_dynamic_suggestions(type_counts: &BTreeMap<String, usize>) -> Vec<Strin
}
}

if type_counts.get("trigger").copied().unwrap_or(0) > 0 {
suggestions.push("workgraph trigger replay".to_owned());
}

suggestions.push("workgraph status".to_owned());
suggestions
}
45 changes: 42 additions & 3 deletions crates/wg-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use anyhow::{Context, anyhow, bail};
use tokio::fs;
use wg_store::{PrimitiveFrontmatter, StoredPrimitive, read_primitive};
use wg_trigger::{TriggerMutationService, load_trigger};
use wg_types::ActorId;

use crate::app::AppContext;
Expand Down Expand Up @@ -99,9 +100,41 @@ pub async fn handle(
.default_actor_id
.unwrap_or_else(|| ActorId::new("cli"));

let (path, ledger_entry) = PrimitiveMutationService::new(app, &registry)
.create(actor, &primitive)
.await?;
let (path, ledger_entry) = if primitive_type == "trigger" {
let trigger = load_trigger_payload(&primitive)?;
TriggerMutationService::new(app.workspace())
.save_trigger_as(&trigger, actor.clone())
.await?;
let stored_trigger = load_trigger(app.workspace(), &trigger.id).await?;
let stored_primitive = read_primitive(app.workspace(), "trigger", &stored_trigger.id)
.await
.with_context(|| {
format!(
"failed to read stored trigger 'trigger/{}'",
stored_trigger.id
)
})?;
primitive = stored_primitive;
let ledger_entry = app
.read_ledger_entries()
.await?
.into_iter()
.rev()
.find(|entry| entry.primitive_type == "trigger" && entry.primitive_id == trigger.id)
.ok_or_else(|| anyhow!("failed to locate ledger entry for trigger/{}", trigger.id))?;
(
app.workspace()
.primitive_path("trigger", &trigger.id)
.as_path()
.display()
.to_string(),
ledger_entry,
)
} else {
PrimitiveMutationService::new(app, &registry)
.create(actor, &primitive)
.await?
};

Ok(CreateOutput {
outcome: CreateOutcome::Created,
Expand Down Expand Up @@ -141,3 +174,9 @@ fn resolve_create_inputs(
};
Ok((title, fields))
}

fn load_trigger_payload(primitive: &StoredPrimitive) -> anyhow::Result<wg_trigger::Trigger> {
let trigger = wg_trigger::trigger_from_primitive(primitive)
.context("failed to decode trigger payload from create request")?;
Ok(trigger)
}
35 changes: 34 additions & 1 deletion crates/wg-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ mod schema;
mod show;
mod status;
mod thread_complete;
mod trigger;

use crate::app::AppContext;
use crate::args::{Command, RunCommand};
use crate::args::{Command, RunCommand, TriggerCommand};
use crate::output::CommandOutput;

/// Executes the selected CLI command using the shared application context.
Expand Down Expand Up @@ -111,5 +112,37 @@ pub async fn execute(app: &AppContext, command: Command) -> anyhow::Result<Comma
run::cancel(app, &run_id, summary.as_deref()).await?,
)),
},
Command::Trigger { command } => match command {
TriggerCommand::Validate { reference } => Ok(CommandOutput::TriggerValidate(
trigger::validate(app, &reference).await?,
)),
TriggerCommand::Replay { last } => Ok(CommandOutput::TriggerReplay(
trigger::replay(app, last).await?,
)),
TriggerCommand::Ingest {
source,
event_id,
event_name,
provider,
fields,
} => Ok(CommandOutput::TriggerIngest(
trigger::ingest(
app,
trigger::TriggerIngestArgs {
source,
event_id: event_id.unwrap_or_else(|| "manual-ingest".to_owned()),
event_name,
provider,
actor_id: trigger::field_value(&fields, "actor_id"),
subject_reference: trigger::field_value(&fields, "subject_reference"),
primitive_type: trigger::field_value(&fields, "primitive_type"),
primitive_id: trigger::field_value(&fields, "primitive_id"),
op: trigger::field_value(&fields, "op"),
fields,
},
)
.await?,
)),
},
}
}
3 changes: 3 additions & 0 deletions crates/wg-cli/src/commands/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,8 @@ pub async fn handle(app: &AppContext) -> anyhow::Result<StatusOutput> {
graph_issues: workspace_status.graph_issues,
orphan_nodes: workspace_status.orphan_nodes,
thread_evidence_gaps: workspace_status.thread_evidence_gaps,
trigger_health: workspace_status.trigger_health,
recent_trigger_receipts: workspace_status.recent_trigger_receipts,
pending_trigger_actions: workspace_status.pending_trigger_actions,
})
}
Loading
Loading