harness-tui + engine: jobs pane, subtask drill-in, reminders (M4)

TUI: Ctrl+J (or /jobs) opens a jobs pane listing the current session's board —
alias, agent, state, objective, files read — populated on load and kept live via
JobUpdated events; Enter drills into a subtask's child session. Snapshot test added.

Engine: optional orchestration reminders (off by default) injected as synthetic,
non-persisted turn-start blocks and, after a file tool runs, an after-file-tool
block on the following turn. Processor reports file-tool usage via StepOutcome.

This completes M4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 06:46:31 +02:00
co-authored by Claude Opus 4.8
parent 54f91b7e4c
commit 7738ed55b9
15 changed files with 426 additions and 28 deletions
+26
View File
@@ -325,6 +325,8 @@ impl EngineHandle {
instructions: self.inner.config.instructions.clone(),
cost: Some(self.inner.catalog.cost(provider_id, model_id)),
inject_job_board,
reminder_turn_start: self.inner.reminder("turn_start"),
reminder_after_file_tool: self.inner.reminder("after_file_tool"),
};
// Reserve the run slot *before* spawning. If we inserted after spawning, a run that
@@ -386,6 +388,18 @@ impl EngineHandle {
Ok(self.inner.store.parts(message_id).await?)
}
pub async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>, AppError> {
Ok(self.inner.store.session(session_id).await?)
}
/// Background jobs spawned by `session_id` (the parent), for the TUI jobs pane / drill-in.
pub async fn jobs(
&self,
session_id: SessionId,
) -> Result<Vec<harness_core::engine::JobRecord>, AppError> {
Ok(self.inner.store.jobs_for_parent(session_id).await?)
}
pub fn is_running(&self, session_id: &SessionId) -> bool {
let runs = self.inner.runs.lock().unwrap();
runs.contains_key(session_id)
@@ -477,6 +491,16 @@ impl EngineInner {
}
}
/// An optional orchestration reminder string by hook key (`turn_start`/`after_file_tool`).
fn reminder(&self, key: &str) -> Option<String> {
self.config
.orchestration
.reminders
.as_ref()?
.get(key)
.cloned()
}
/// The ruleset a session evaluates its own tool calls against: low-priority depth guards
/// (depth > 0), then global config permissions, then the agent's own rules (highest).
fn effective_static_rules(&self, agent: &AgentDef, depth: u8) -> Ruleset {
@@ -712,6 +736,8 @@ impl SubagentSpawner for EngineInner {
&child_session.model.model_id,
)),
inject_job_board: agent.mode.is_primary(),
reminder_turn_start: self.reminder("turn_start"),
reminder_after_file_tool: self.reminder("after_file_tool"),
};
// Register the launch on the parent board (also makes foreground results reusable).