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
15 changes: 15 additions & 0 deletions apps/decodex/src/execution_program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,16 @@ pub(crate) enum ExecutionQueueLabelAction {
/// Remove the service queue label.
Remove,
}
impl ExecutionQueueLabelAction {
/// Stable machine-readable queue-label action.
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Apply => "apply",
Self::Retain => "retain",
Self::Remove => "remove",
}
}
}

/// Conflict-domain key for one program node.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
Expand Down Expand Up @@ -1726,6 +1736,11 @@ fn lifecycle_state_for(
{
return ExecutionProgramNodeLifecycleState::NeedsAttention;
}
if let Some(issue) = node.linear_issue()
&& issue.has_active_label
{
return ExecutionProgramNodeLifecycleState::Active;
}

match state {
ExecutionReadinessState::NotReady | ExecutionReadinessState::Paused =>
Expand Down
4 changes: 2 additions & 2 deletions apps/decodex/src/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub(crate) use lane_control::{
#[cfg(unix)] use std::os::fd::AsRawFd;
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
collections::{self, BTreeMap, BTreeSet, HashMap, HashSet},
env,
error::Error,
fmt::{self, Display, Formatter},
Expand All @@ -40,7 +40,7 @@ use time::{OffsetDateTime, format_description::well_known::Rfc3339};

use crate::{agent, default_branch_sync, git_credentials, maintenance, state};
#[rustfmt::skip]
use crate::{agent::{ACTIVE_RUN_IDLE_TIMEOUT, AppServerCapabilityPreflightFailure, AppServerDynamicToolFailure, AppServerHomePreflightFailure, AppServerPhaseGoalFailure, AppServerProcessEnv, AppServerRunRequest, AppServerRunResult, AppServerTransportFailure, AppServerTurnFailure, ISSUE_DELIVERY_CLOSEOUT_COMPLETE_TOOL_NAME, ISSUE_LABEL_ADD_TOOL_NAME, ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME, ISSUE_REVIEW_CHECKPOINT_TOOL_NAME, ISSUE_REVIEW_HANDOFF_TOOL_NAME, ISSUE_REVIEW_REPAIR_COMPLETE_TOOL_NAME, ISSUE_TERMINAL_FINALIZE_TOOL_NAME, ISSUE_TRANSITION_TOOL_NAME, DecodexRunContext, DecodexToolBridge, PhaseGoalController, PhaseGoalKind, PhaseGoalSpec, PhaseGoalTransition, ReviewExecutionMode, ReviewHandoffContext, ReviewHandoffWritebackFailed, ReviewPolicyStopReason, ReviewPolicyStopRequested, RunCompletionDisposition, TrackerToolBridge, TurnContinuationGuard}, config::{ReviewLevel, ServiceConfig}, execution_program::{ExecutionProgramOperatorSummary, ExecutionProgramReadinessContext, ExecutionWorkflowPolicy}, git_credentials::GitCredentialSource, github, prelude::{Result, eyre}, state::{ChildAgentActivityBucket, ChildAgentActivitySummary, CodexAccountActivitySummary, ExecutionProgramRecord, LoopGuardrailCheckpoint, LoopGuardrailCheckpointInput, ProjectRegistration, ProjectRunStatus, ProtocolActivitySummary, RUN_OPERATION_AGENT_RUN, RUN_OPERATION_APP_SERVER_PREFLIGHT, RUN_OPERATION_GIT_CREDENTIALS, RUN_OPERATION_IDLE, RUN_OPERATION_RECONCILIATION, RUN_OPERATION_REPO_GATE, RUN_OPERATION_REVIEW_WRITEBACK, RUN_OPERATION_WAITING_EXTERNAL, ReviewHandoffMarker, ReviewOrchestrationMarker, RunActivityMarker, RunAttempt, StateStore, WorktreeMapping}, tracker::{IssueTracker, TrackerComment, TrackerIssue, linear::LinearClient, records}, workflow::{WorkflowDocument, WorkflowExecution}, worktree::{WorktreeManager, WorktreeSpec}};
use crate::{agent::{ACTIVE_RUN_IDLE_TIMEOUT, AppServerCapabilityPreflightFailure, AppServerDynamicToolFailure, AppServerHomePreflightFailure, AppServerPhaseGoalFailure, AppServerProcessEnv, AppServerRunRequest, AppServerRunResult, AppServerTransportFailure, AppServerTurnFailure, ISSUE_DELIVERY_CLOSEOUT_COMPLETE_TOOL_NAME, ISSUE_LABEL_ADD_TOOL_NAME, ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME, ISSUE_REVIEW_CHECKPOINT_TOOL_NAME, ISSUE_REVIEW_HANDOFF_TOOL_NAME, ISSUE_REVIEW_REPAIR_COMPLETE_TOOL_NAME, ISSUE_TERMINAL_FINALIZE_TOOL_NAME, ISSUE_TRANSITION_TOOL_NAME, DecodexRunContext, DecodexToolBridge, PhaseGoalController, PhaseGoalKind, PhaseGoalSpec, PhaseGoalTransition, ReviewExecutionMode, ReviewHandoffContext, ReviewHandoffWritebackFailed, ReviewPolicyStopReason, ReviewPolicyStopRequested, RunCompletionDisposition, TrackerToolBridge, TurnContinuationGuard}, config::{ReviewLevel, ServiceConfig}, execution_program::{ExecutionNodeEvaluation, ExecutionProgramEvaluation, ExecutionProgramOperatorSummary, ExecutionProgramReadinessContext, ExecutionQueueLabelAction, ExecutionWorkflowPolicy}, git_credentials::GitCredentialSource, github, prelude::{Result, eyre}, state::{ChildAgentActivityBucket, ChildAgentActivitySummary, CodexAccountActivitySummary, ExecutionProgramRecord, LoopGuardrailCheckpoint, LoopGuardrailCheckpointInput, ProjectRegistration, ProjectRunStatus, ProtocolActivitySummary, RUN_OPERATION_AGENT_RUN, RUN_OPERATION_APP_SERVER_PREFLIGHT, RUN_OPERATION_GIT_CREDENTIALS, RUN_OPERATION_IDLE, RUN_OPERATION_RECONCILIATION, RUN_OPERATION_REPO_GATE, RUN_OPERATION_REVIEW_WRITEBACK, RUN_OPERATION_WAITING_EXTERNAL, ReviewHandoffMarker, ReviewOrchestrationMarker, RunActivityMarker, RunAttempt, StateStore, WorktreeMapping}, tracker::{IssueTracker, TrackerComment, TrackerIssue, linear::LinearClient, records}, workflow::{WorkflowDocument, WorkflowExecution}, worktree::{WorktreeManager, WorktreeSpec}};
use harness_improvement::{
HarnessImprovementCandidateSummary, HarnessOutcomeKind,
harness_improvement_candidates_from_private_events, record_harness_outcome_best_effort,
Expand Down
2 changes: 0 additions & 2 deletions apps/decodex/src/orchestrator/agent_evidence.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::collections::{self, BTreeMap};

const AGENT_HANDOFF_INDEX_SCHEMA: &str = "decodex.agent_handoff_index/1";
const AGENT_BLOCKER_SNAPSHOT_SCHEMA: &str = "decodex.blocker_snapshot/1";
const AGENT_RUN_CAPSULE_SCHEMA: &str = "decodex.run_capsule/1";
Expand Down
2 changes: 1 addition & 1 deletion apps/decodex/src/orchestrator/git_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ mod repo_gate_failure {
}
}

use std::{collections::BTreeSet, process::Output};
use std::process::Output;

use repo_gate_failure::{RepoGateFailure, RepoGateFailureDisposition, RepoGateFailureKind};
use crate::workflow::ResolvedRepoGate;
Expand Down
211 changes: 209 additions & 2 deletions apps/decodex/src/orchestrator/operator_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -3603,6 +3603,18 @@ <h2 id="active-title">Running Lanes</h2>
</div>
</section>

<section class="panel section-execution" id="programs-panel">
<div class="panel-head">
<div>
<h2 id="programs-title">Program Intake</h2>
</div>
<p class="table-meta" id="programs-meta"></p>
</div>
<div class="panel-body">
<div class="card-list" id="execution-programs"></div>
</div>
</section>

<section class="panel section-execution" id="queue-panel">
<div class="panel-head">
<div>
Expand Down Expand Up @@ -3786,6 +3798,7 @@ <h2 id="recent-title">Run History</h2>
projects: document.getElementById("projects-panel"),
accountPool: document.getElementById("account-pool-panel"),
active: document.getElementById("active-panel"),
programs: document.getElementById("programs-panel"),
queue: document.getElementById("queue-panel"),
recent: document.getElementById("recent-panel"),
review: document.getElementById("review-panel"),
Expand All @@ -3804,6 +3817,8 @@ <h2 id="recent-title">Run History</h2>
queuedMeta: document.getElementById("queued-meta"),
activeRuns: document.getElementById("active-runs"),
activeRunsMeta: document.getElementById("active-runs-meta"),
executionPrograms: document.getElementById("execution-programs"),
programsMeta: document.getElementById("programs-meta"),
recentRuns: document.getElementById("recent-runs"),
recentRunsMeta: document.getElementById("recent-runs-meta"),
reviewQueue: document.getElementById("review-queue"),
Expand Down Expand Up @@ -3849,12 +3864,12 @@ <h2 id="recent-title">Run History</h2>
const detailAnimationTimers = new WeakMap();

const DASHBOARD_LAYOUT = {
primary: ["accountPool", "projects", "active", "queue", "review", "worktrees", "recent"],
primary: ["accountPool", "projects", "active", "programs", "queue", "review", "worktrees", "recent"],
};
const DASHBOARD_SECTION_GROUPS = [
{ marker: "control", panels: ["accountPool"] },
{ marker: "projects", panels: ["projects"] },
{ marker: "execution", panels: ["active", "queue"] },
{ marker: "execution", panels: ["active", "programs", "queue"] },
{ marker: "aftercare", panels: ["review", "worktrees", "recent"] },
];
const COPY = {
Expand Down Expand Up @@ -8656,6 +8671,7 @@ <h2 id="recent-title">Run History</h2>
const activeRuns = snapshot?.active_runs ?? [];
const recentRuns = snapshot?.recent_runs ?? [];
const historyRuns = sessionHistoryRuns(snapshot);
const executionPrograms = snapshot?.execution_programs ?? [];
const postReviewLanes = snapshot?.post_review_lanes ?? [];
const postReviewIssueKeys = new Set(postReviewLanes.flatMap(issueIdentityKeys));
const activeRunByIssue = new Map();
Expand Down Expand Up @@ -8878,6 +8894,17 @@ <h2 id="recent-title">Run History</h2>
const runningAttentionCount = attentionItems.filter((item) => item.scope === "Running").length;
const liveRuns = activeRuns.filter(runCountsAsRunning).length;
const intakeAttentionCount = queueBacklogCandidates.filter(queuedCandidateNeedsAttention).length;
const programAttentionCount = executionPrograms.filter((program) =>
["attention", "blocked", "stale"].includes(program.status),
).length;
const programReadyCount = executionPrograms.reduce(
(total, program) => total + Number(program.ready_count || 0),
0,
);
const programQueuedCount = executionPrograms.reduce(
(total, program) => total + Number(program.queued_count || 0),
0,
);

return {
projects: snapshot?.projects ?? [],
Expand Down Expand Up @@ -8905,6 +8932,10 @@ <h2 id="recent-title">Run History</h2>
reviewBlockerCount,
cleanupCount,
runningAttentionCount,
executionPrograms,
programAttentionCount,
programReadyCount,
programQueuedCount,
sessionHistoryRuns: historyRuns,
worktrees,
postReviewLanes,
Expand Down Expand Up @@ -9523,6 +9554,181 @@ <h2 id="recent-title">Run History</h2>
return parts.join(" · ");
}

function programMetaText(snapshot, derived) {
if (!snapshot) {
return "0 programs";
}

const parts = [pluralize(derived.executionPrograms.length, "program")];

if (derived.programReadyCount) {
parts.push(`${derived.programReadyCount} ready`);
}
if (derived.programQueuedCount) {
parts.push(`${derived.programQueuedCount} queued`);
}
if (derived.programAttentionCount) {
parts.push(
derived.programAttentionCount === 1
? "1 needs attention"
: `${derived.programAttentionCount} need attention`,
);
}

return parts.join(" · ");
}

function toneForProgram(program) {
switch (program.status) {
case "ready":
case "completed":
return "tone-ready";
case "queued":
return "tone-queue";
case "active":
return "tone-run";
case "blocked":
case "attention":
case "stale":
return "tone-blocked";
case "held":
return "tone-wait";
default:
return "tone-muted";
}
}

function programMappedIssues(program) {
const issues = program.mapped_issue_identifiers ?? [];

return issues.length ? issues.join(", ") : "NONE";
}

function programQueueActionSummary(program) {
const actions = [
["apply", program.queue_label_apply_count],
["retain", program.queue_label_retain_count],
["remove", program.queue_label_remove_count],
].filter(([, count]) => Number(count || 0) > 0);

return actions.length
? actions.map(([label, count]) => `${label} ${count}`).join(", ")
: "none";
}

function programProgressFacts(program) {
return [
["Ready", String(program.ready_count ?? 0)],
["Queued", String(program.queued_count ?? 0)],
["Active", String(program.active_count ?? 0)],
["Blocked", String(program.blocked_count ?? 0)],
["Held", String(program.held_count ?? 0)],
["Attention", String(program.needs_attention_count ?? 0)],
["Completed", String(program.completed_count ?? 0)],
["Stale", String(program.stale_count ?? 0)],
["Eligible", String(program.queue_label_eligible_count ?? 0)],
];
}

function programNodeReasons(node) {
const reasons = node.reasons ?? [];
if (reasons.length) {
return reasons.join("; ");
}

const reasonCodes = node.reason_codes ?? [];
return reasonCodes.length ? reasonCodes.map(displayToken).join(", ") : "none";
}

function programNodeIssue(node) {
return node.issue_identifier || "unmapped";
}

function renderProgramNodeReadbacks(program) {
const readbacks = program.node_readbacks ?? [];
if (!readbacks.length) {
return "";
}

const detailKey = `program:${program.program_id}:nodes`;

return `
<details data-detail-key="${escapeHtml(detailKey)}"${detailsOpenAttribute(detailKey)}>
<summary>${escapeHtml(pluralize(readbacks.length, "node diagnostic"))}</summary>
<div class="grid debug-grid">
${readbacks
.map((node) => {
const reasonCodes = (node.reason_codes ?? []).map(displayToken).join(", ") || "none";
return `
${field("Issue", programNodeIssue(node))}
${field("Lifecycle", displayToken(node.lifecycle_state || "unknown"))}
${field("Readiness", displayToken(node.readiness_state || "unknown"))}
${field("Issue state", node.issue_state || "none")}
${field("Queue action", node.queue_label_action || "none")}
${field("Reason codes", reasonCodes)}
${field("Reasons", programNodeReasons(node))}
${field("Next action", node.next_action || "none")}
`;
})
.join("")}
</div>
</details>
`;
}

function renderExecutionPrograms(snapshot, derived) {
const programs = derived.executionPrograms ?? [];
setPanelMeta(
nodes.programsMeta,
programMetaText(snapshot, derived),
derived.programAttentionCount ? "attention" : "",
);

if (!programs.length) {
renderRoutineEmptyList(nodes.executionPrograms);
return;
}

renderStableList(
nodes.executionPrograms,
programs
.map((program) => {
const tone = toneForProgram(program);
const mappedIssues = programMappedIssues(program);
const warning = program.readback_warning
? inlineStatusFact("Warning", displayToken(program.readback_warning))
: "";
return `
<article class="action-card ${tone}" data-render-key="program:${escapeHtml(program.program_id)}">
<div class="row-head">
<div class="row-title">
<div class="kicker">
<span>${escapeHtml(displayToken(program.intake_kind || "program"))}</span>
<span class="mono">${escapeHtml(program.program_id)}</span>
</div>
<h4>${escapeHtml(program.public_summary || program.program_id)}</h4>
</div>
</div>
<div class="status-line">
${statusLabel(displayToken(program.status || "unknown"), tone)}
${inlineStatusFact("Queue", programQueueActionSummary(program))}
${warning}
</div>
<div class="grid two card-facts">
${cardField("Mapped issues", mappedIssues, mappedIssues === "NONE" ? "is-muted" : "")}
${cardField("Source contract", program.source_contract_id || "NONE", program.source_contract_id ? "" : "is-muted")}
${programProgressFacts(program)
.map(([label, value]) => cardField(label, value))
.join("")}
</div>
${renderProgramNodeReadbacks(program)}
</article>
`;
})
.join(""),
);
}

function renderQueuedCandidates(container, items) {
if (!items.length) {
renderRoutineEmptyList(container);
Expand Down Expand Up @@ -10637,6 +10843,7 @@ <h4>${escapeHtml(worktree.branch_name)}</h4>
renderProjects(snapshot, derived);
renderAccountPool(snapshot);
renderActiveRuns(snapshot, derived);
renderExecutionPrograms(snapshot, derived);
renderQueuedCandidates(
nodes.queuedCandidates,
derived.queueBacklogCandidates,
Expand Down
3 changes: 1 addition & 2 deletions apps/decodex/src/orchestrator/program_reconciler.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::execution_program::{
ExecutionDependencySnapshot, ExecutionLinearIssueMapping, ExecutionNodeEvaluation,
ExecutionProgram, ExecutionQueueLabelAction,
ExecutionDependencySnapshot, ExecutionLinearIssueMapping, ExecutionProgram,
};
use crate::execution_program::ExecutionConflictDomain;

Expand Down
Loading