mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
refactor(goal): the model decides goals; the host stops guessing from verb lists
Founder clarification, 2026-09-09 (docs/design/TUI_DECONSTRUCTION.md): "the harness should let the model decide when a goal, plan, delegation, further investigation, or verification is useful." Operate violated that with three conflicting authorities: `operate_goal_from_prompt` classified instructions with work-verb and chat-opener lists, `CreateGoalTool::description` said goals require an explicit request, and the host promoted a prompt before the model ever ran. The verb list produced exactly the absurdity it invites: "test respond with hello" became a persistent goal because "test" is a verb. - `operate_goal_from_prompt` and its vocabularies are deleted (~8.6 KB with tests). The engine creates a goal only from an explicit user declaration — `/goal` and the natural-language forms `explicit_goal_directive` parses — or when the model calls `create_goal`. No wording is classified. - `create_goal`'s description becomes the one model-facing contract: the model decides when a request is a durable objective, and is told what is not one (a question, a greeting, a one-shot edit, a conversational probe). - The create-refusal report is now always the explicit one: a `/goal` that `GoalState::create` refuses says so instead of being swallowed. - docs/MODES.md (Operate) updated to match; the design doc's "conflicting authorities" note is resolved by this change. Tests updated to the contract, not the old behavior: - `operate_never_promotes_wording_to_a_goal`: ordinary work prompts are ordinary turns in Operate and Work; an explicit declaration still creates the goal. - `operate_contract_is_appended_once_and_an_existing_goal_is_never_replaced` seeds its goal through the explicit declaration. - `operate_model_shell_uses_normal_approval_and_workspace_sandbox` declares its goal explicitly instead of relying on promotion. Evidence: goal filter "148 passed; 0 failed"; operate filter "55 passed; 0 failed"; clippy (CI's invocation, tui lib) clean.
This commit is contained in:
1 parent
c191943cda
commit
4de9e9e281
4 files changed
+36
-286
No files matched your search
@@ -41,7 +41,7 @@ use crate::route_runtime::{
|
||||
};
|
||||
use crate::tools::goal::{
|
||||
GoalPauseReason, GoalSnapshot, GoalStatus, SharedGoalState, explicit_goal_directive,
|
||||
new_shared_goal_state, operate_goal_from_prompt,
|
||||
new_shared_goal_state,
|
||||
};
|
||||
use crate::tools::plan::{SharedPlanState, new_shared_plan_state};
|
||||
use crate::tools::shell::{SharedShellManager, new_shared_shell_manager};
|
||||
@@ -4866,9 +4866,11 @@ impl Engine {
|
||||
// runtime text, recalled memory, handoffs, and pasted multi-line
|
||||
// transcripts cannot create a goal.
|
||||
//
|
||||
// Operate turns an ordinary work prompt into the goal through this
|
||||
// same path when the host reports no unfinished goal
|
||||
// (`operate_goal_from_prompt` owns the "is this real work?" rule).
|
||||
// Goals are created by the model (`create_goal`) or by this literal
|
||||
// user declaration; the host never infers one from wording. The
|
||||
// verb-list promotion that used to turn ordinary Operate prompts into
|
||||
// goals is gone (docs/design/TUI_DECONSTRUCTION.md — founder
|
||||
// clarification 2026-09-09: the model decides when a goal is useful).
|
||||
// `GoalState::create` still refuses while an unfinished goal exists,
|
||||
// so a paused or blocked goal is never silently replaced.
|
||||
//
|
||||
@@ -4876,19 +4878,10 @@ impl Engine {
|
||||
// <session_goal> contributor. It adds no new stable-prefix text.
|
||||
let goal_request = if provenance.can_authorize_work() {
|
||||
explicit_goal_directive(&content)
|
||||
.map(|directive| (directive, true))
|
||||
.or_else(|| {
|
||||
let host_goal_unfinished =
|
||||
goal_objective.is_some() && goal_status != GoalStatus::Complete;
|
||||
(mode == AppMode::Operate && !host_goal_unfinished)
|
||||
.then(|| operate_goal_from_prompt(&content))
|
||||
.flatten()
|
||||
.map(|directive| (directive, false))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((directive, explicit)) = goal_request {
|
||||
if let Some(directive) = goal_request {
|
||||
let result = self
|
||||
.config
|
||||
.goal_state
|
||||
@@ -4910,7 +4903,7 @@ impl Engine {
|
||||
// even when this model would otherwise reply only in prose.
|
||||
let _ = self.tx_event.send(Event::GoalUpdated { snapshot }).await;
|
||||
}
|
||||
Err(error) if explicit => {
|
||||
Err(error) => {
|
||||
let _ = self
|
||||
.tx_event
|
||||
.send(Event::status(format!(
|
||||
@@ -4918,8 +4911,6 @@ impl Engine {
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
// Operate keeps the unfinished goal it already has.
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3375,17 +3375,19 @@ async fn operate_goal_probe(mode: AppMode, prompt: &str) -> (Option<String>, boo
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn operate_turns_a_work_prompt_into_the_goal_but_work_mode_does_not() {
|
||||
async fn operate_never_promotes_wording_to_a_goal() {
|
||||
let prompt =
|
||||
"Migrate the settings loader to the new config crate and keep the old keys readable";
|
||||
|
||||
// The verb-list promotion is gone: an ordinary work prompt is an ordinary
|
||||
// turn in every mode, and the model decides goals through `create_goal`
|
||||
// (docs/design/TUI_DECONSTRUCTION.md, founder clarification 2026-09-09).
|
||||
let (objective, active, contracts) = operate_goal_probe(AppMode::Operate, prompt).await;
|
||||
assert_eq!(
|
||||
objective.as_deref(),
|
||||
Some(prompt),
|
||||
"Operate must publish the prompt as the goal before the provider call"
|
||||
objective, None,
|
||||
"the host must not infer a goal from wording"
|
||||
);
|
||||
assert!(active, "Operate goal must be active in engine state");
|
||||
assert!(!active);
|
||||
assert_eq!(contracts, 1, "Operate appends its contract exactly once");
|
||||
|
||||
let (objective, active, contracts) = operate_goal_probe(AppMode::Agent, prompt).await;
|
||||
@@ -3393,11 +3395,12 @@ async fn operate_turns_a_work_prompt_into_the_goal_but_work_mode_does_not() {
|
||||
assert!(!active);
|
||||
assert_eq!(contracts, 0, "Work never sees the Operate contract");
|
||||
|
||||
// An explicit declaration still creates one, through the same path.
|
||||
let (objective, active, contracts) =
|
||||
operate_goal_probe(AppMode::Operate, "thanks, looks good").await;
|
||||
assert_eq!(objective, None, "chat stays chat even in Operate");
|
||||
assert!(!active);
|
||||
assert_eq!(contracts, 1, "the contract is about the mode, not the goal");
|
||||
operate_goal_probe(AppMode::Operate, "Please set /goal to ship the release").await;
|
||||
assert_eq!(objective.as_deref(), Some("ship the release"));
|
||||
assert!(active, "an explicit declaration must create the goal");
|
||||
assert_eq!(contracts, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3502,10 +3505,11 @@ async fn operate_contract_is_appended_once_and_an_existing_goal_is_never_replace
|
||||
})
|
||||
};
|
||||
|
||||
let first =
|
||||
let first_objective =
|
||||
"Migrate the settings loader to the new config crate and keep the old keys readable";
|
||||
let first = format!("Please set /goal to {first_objective}");
|
||||
handle
|
||||
.send(send(first, None, crate::tools::goal::GoalStatus::Active))
|
||||
.send(send(&first, None, crate::tools::goal::GoalStatus::Active))
|
||||
.await
|
||||
.expect("send first Operate turn");
|
||||
tokio::time::timeout(model_turn_event_timeout(), first_entered.notified())
|
||||
@@ -3513,7 +3517,7 @@ async fn operate_contract_is_appended_once_and_an_existing_goal_is_never_replace
|
||||
.expect("first provider request was never entered");
|
||||
assert_eq!(
|
||||
goal_state.lock().expect("goal lock").objective(),
|
||||
Some(first)
|
||||
Some(first_objective)
|
||||
);
|
||||
handle
|
||||
.send(Op::SetGoalStatus {
|
||||
@@ -3534,7 +3538,7 @@ async fn operate_contract_is_appended_once_and_an_existing_goal_is_never_replace
|
||||
handle
|
||||
.send(send(
|
||||
"Refactor the provider table so it survives a config reload",
|
||||
Some(first.to_string()),
|
||||
Some(first_objective.to_string()),
|
||||
crate::tools::goal::GoalStatus::Paused,
|
||||
))
|
||||
.await
|
||||
@@ -3554,7 +3558,7 @@ async fn operate_contract_is_appended_once_and_an_existing_goal_is_never_replace
|
||||
"the contract must not repeat on later Operate turns"
|
||||
);
|
||||
let goal = goal_state.lock().expect("goal lock").snapshot();
|
||||
assert_eq!(goal.objective.as_deref(), Some(first));
|
||||
assert_eq!(goal.objective.as_deref(), Some(first_objective));
|
||||
assert_eq!(goal.status, "paused");
|
||||
|
||||
handle.send(Op::Shutdown).await.expect("shutdown engine");
|
||||
@@ -12368,9 +12372,10 @@ async fn operate_model_shell_uses_normal_approval_and_workspace_sandbox() {
|
||||
"\"finish_reason\":\"stop\"}]}\n\n",
|
||||
"data: [DONE]\n\n",
|
||||
);
|
||||
// Operate turns the work prompt into a goal, so after the approved shell
|
||||
// runs, the model must seal the goal through the same `update_goal` tool
|
||||
// a live Operate turn uses; only then does the final "done" arrive.
|
||||
// Operate no longer infers a goal from wording, so this fixture declares
|
||||
// one explicitly; after the approved shell runs, the model seals it
|
||||
// through the same `update_goal` tool a live Operate turn uses, and only
|
||||
// then does the final "done" arrive.
|
||||
let goal_seal_marker = "goal-seal-receipt-0902";
|
||||
let goal_sse = concat!(
|
||||
"data: {\"id\":\"chatcmpl-operate-goal\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[",
|
||||
@@ -12456,7 +12461,7 @@ async fn operate_model_shell_uses_normal_approval_and_workspace_sandbox() {
|
||||
handle
|
||||
.send(Op::SendMessage(TurnSpec {
|
||||
max_output_tokens: None,
|
||||
content: "write the requested local fixture".to_string(),
|
||||
content: "Please set /goal to write the requested local fixture".to_string(),
|
||||
images: Vec::new(),
|
||||
mode: AppMode::Operate,
|
||||
route: resolved_route_for_test(&api_config, crate::config::DEFAULT_TEXT_MODEL),
|
||||
@@ -12494,19 +12499,9 @@ async fn operate_model_shell_uses_normal_approval_and_workspace_sandbox() {
|
||||
Event::ApprovalRequired { id, tool_name, .. } => {
|
||||
saw_approval = true;
|
||||
assert_eq!(tool_name, "Bash");
|
||||
// Operate turned this work prompt into the goal; pause it
|
||||
// before the turn ends so the goal-continuation loop cannot
|
||||
// queue passes nobody answers in this mock. The goal-complete
|
||||
// `update_goal` response below stays: it is the receipt a
|
||||
// real Operate turn seals with.
|
||||
handle_for_approval
|
||||
.send(Op::SetGoalStatus {
|
||||
goal_id: None,
|
||||
status: crate::tools::goal::GoalStatus::Paused,
|
||||
clear: false,
|
||||
})
|
||||
.await
|
||||
.expect("queue goal pause");
|
||||
// No goal is created for this prompt: goals are model-decided
|
||||
// (`create_goal`), so there is no goal-continuation loop to
|
||||
// pause in this mock.
|
||||
handle_for_approval
|
||||
.approve_tool_call(id)
|
||||
.await
|
||||
|
||||
@@ -154,149 +154,6 @@ fn normalize_explicit_goal_objective(raw: &str) -> Option<String> {
|
||||
(!objective.is_empty()).then(|| objective.to_string())
|
||||
}
|
||||
|
||||
/// Operate's automatic goal: Operate turns an ordinary work prompt into the
|
||||
/// session goal when no unfinished goal exists (docs/MODES.md, "Operate loop").
|
||||
/// It feeds the same `GoalState::create` path as [`explicit_goal_directive`];
|
||||
/// an explicit `/goal` declaration always wins, and Plan and Work never call
|
||||
/// this.
|
||||
///
|
||||
/// Only a direct work instruction is considered for automatic persistence.
|
||||
/// Prompt length alone never authorizes a goal: questions and conversational
|
||||
/// followups and requests declining a goal remain ordinary turns. Ambiguous requests still run normally;
|
||||
/// the user can use `/goal` to explicitly request persistent work.
|
||||
#[must_use]
|
||||
pub fn operate_goal_from_prompt(input: &str) -> Option<ExplicitGoalDirective> {
|
||||
let input = input.trim();
|
||||
if input.starts_with(['"', '\'', '`', '>']) {
|
||||
return None;
|
||||
}
|
||||
// Mode defaults cannot override a user's request to avoid persistence.
|
||||
// Keep this conservative: declining an automatic goal still runs the task.
|
||||
let lower = input.to_lowercase().replace('’', "'");
|
||||
if [
|
||||
"no goal",
|
||||
"without a goal",
|
||||
"without goal",
|
||||
"不要创建目标",
|
||||
"不要设置目标",
|
||||
]
|
||||
.iter()
|
||||
.any(|phrase| lower.contains(phrase))
|
||||
|| lower
|
||||
.split(['.', '!', '?', ';', '。', '!', '?', ';'])
|
||||
.any(|clause| {
|
||||
["do not ", "don't ", "never ", "without "]
|
||||
.iter()
|
||||
.filter_map(|negation| clause.find(negation))
|
||||
.any(|start| {
|
||||
// A negated list carries through commas: "do not edit
|
||||
// files, create a goal, or start another tool".
|
||||
let words: Vec<_> = clause[start..]
|
||||
.split(|c: char| !c.is_alphanumeric())
|
||||
.filter(|word| !word.is_empty())
|
||||
.collect();
|
||||
words.iter().any(|word| matches!(*word, "goal" | "goals"))
|
||||
&& words.iter().any(|word| {
|
||||
matches!(
|
||||
*word,
|
||||
"create"
|
||||
| "creating"
|
||||
| "set"
|
||||
| "setting"
|
||||
| "start"
|
||||
| "starting"
|
||||
| "track"
|
||||
| "tracking"
|
||||
| "use"
|
||||
| "using"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let words: Vec<&str> = input.split_whitespace().collect();
|
||||
if words.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let normalize = |word: &str| {
|
||||
word.trim_matches(|c: char| !c.is_alphanumeric())
|
||||
.to_ascii_lowercase()
|
||||
};
|
||||
let head = words
|
||||
.iter()
|
||||
.map(|word| normalize(word))
|
||||
.find(|word| word != "please")
|
||||
.unwrap_or_default();
|
||||
if OPERATE_CHAT_OPENERS.contains(&head.as_str()) {
|
||||
return None;
|
||||
}
|
||||
let first_line = input.lines().find(|line| !line.trim().is_empty())?.trim();
|
||||
if first_line.ends_with(['?', '?']) || input.ends_with(['?', '?']) {
|
||||
return None;
|
||||
}
|
||||
if !OPERATE_WORK_VERBS.contains(&head.as_str()) {
|
||||
return None;
|
||||
}
|
||||
let mut objective = words.join(" ");
|
||||
if objective.chars().count() > OPERATE_GOAL_MAX_OBJECTIVE_CHARS {
|
||||
objective = objective
|
||||
.chars()
|
||||
.take(OPERATE_GOAL_MAX_OBJECTIVE_CHARS)
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_string();
|
||||
objective.push('…');
|
||||
}
|
||||
Some(ExplicitGoalDirective { objective })
|
||||
}
|
||||
|
||||
const OPERATE_GOAL_MAX_OBJECTIVE_CHARS: usize = 600;
|
||||
const OPERATE_CHAT_OPENERS: &[&str] = &[
|
||||
"hi", "hello", "hey", "thanks", "thank", "ok", "okay", "yes", "no", "sure", "great", "cool",
|
||||
"nice", "lol",
|
||||
];
|
||||
const OPERATE_WORK_VERBS: &[&str] = &[
|
||||
"add",
|
||||
"build",
|
||||
"change",
|
||||
"clean",
|
||||
"convert",
|
||||
"create",
|
||||
"debug",
|
||||
"deploy",
|
||||
"design",
|
||||
"document",
|
||||
"extract",
|
||||
"finish",
|
||||
"fix",
|
||||
"implement",
|
||||
"improve",
|
||||
"integrate",
|
||||
"investigate",
|
||||
"make",
|
||||
"migrate",
|
||||
"move",
|
||||
"optimize",
|
||||
"port",
|
||||
"refactor",
|
||||
"remove",
|
||||
"rename",
|
||||
"replace",
|
||||
"resolve",
|
||||
"rewrite",
|
||||
"run",
|
||||
"ship",
|
||||
"split",
|
||||
"test",
|
||||
"update",
|
||||
"upgrade",
|
||||
"verify",
|
||||
"wire",
|
||||
"write",
|
||||
];
|
||||
|
||||
/// Runtime status for a goal.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum GoalStatus {
|
||||
@@ -1129,7 +986,7 @@ impl ToolSpec for CreateGoalTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. Call this only when the user explicitly asks to use `/goal`, make an objective the goal, or otherwise explicitly requests persistent goal tracking. When the request is explicit, call `create_goal` before doing the rest of the work; acknowledging it in prose is not sufficient. Never infer a goal from an ordinary task, its apparent length, a question, or a one-file edit. Keep the user's full objective, not a shortened one-turn version. Set token_budget only when the user explicitly provides one. Creating a goal shows the user a one-line receipt (they can /goal pause or /goal clear); do not also ask for confirmation. Only one unfinished goal exists at a time: complete or clear it before creating another."
|
||||
"Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. You decide when a request is a durable objective worth carrying across turns — a multi-step outcome the user will want continued and verified. Do not create a goal for a question, a greeting, a one-shot edit, or a conversational probe; those are ordinary turns. When the user explicitly asks to use `/goal` or asks you to make something the goal, call `create_goal` before doing the rest of the work; acknowledging it in prose is not sufficient. Keep the user's full objective, not a shortened one-turn version. Set token_budget only when the user explicitly provides one. Creating a goal shows the user a one-line receipt (they can /goal pause or /goal clear); do not also ask for confirmation. Only one unfinished goal exists at a time: complete or clear it before creating another."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
@@ -1478,99 +1335,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operate_goal_from_prompt_promotes_work_and_leaves_chat_alone() {
|
||||
let goal = operate_goal_from_prompt(
|
||||
"Migrate the settings loader to the new config crate\nand keep the old keys readable.",
|
||||
)
|
||||
.expect("multi-line work prompt");
|
||||
assert_eq!(
|
||||
goal.objective,
|
||||
"Migrate the settings loader to the new config crate and keep the old keys readable."
|
||||
);
|
||||
assert_eq!(
|
||||
operate_goal_from_prompt("Please fix the flaky CI test")
|
||||
.expect("imperative after please")
|
||||
.objective,
|
||||
"Please fix the flaky CI test"
|
||||
);
|
||||
for chat in [
|
||||
"hi",
|
||||
"thanks, looks good",
|
||||
"ok go ahead",
|
||||
"hello there, how are you doing today my friend",
|
||||
"what does /goal do?",
|
||||
"why is the build slow?",
|
||||
"the tests",
|
||||
] {
|
||||
assert_eq!(
|
||||
operate_goal_from_prompt(chat),
|
||||
None,
|
||||
"must stay chat: {chat}"
|
||||
);
|
||||
}
|
||||
let long = "fix ".repeat(400);
|
||||
let objective = operate_goal_from_prompt(&long).expect("bounded").objective;
|
||||
assert!(objective.chars().count() <= OPERATE_GOAL_MAX_OBJECTIVE_CHARS + 1);
|
||||
assert!(objective.ends_with('…'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operate_does_not_promote_conversation_by_length_or_quoted_commands() {
|
||||
for prompt in [
|
||||
"what about like rust or docker builds or something",
|
||||
"what about like rust or docker builds or something?",
|
||||
"why did the build fail on the last step when running docker on macos",
|
||||
"Could you look at why the provider table drops rows after reload and repair it?",
|
||||
"the Rust and Docker builds might explain the disk usage we saw",
|
||||
"explain how the goal loop decides whether to continue working",
|
||||
"那 rust 或者 docker 构建呢",
|
||||
"请问这个模块的具体实现原理是什么以及它如何与其他服务交互",
|
||||
"修复这个问题需要什么步骤",
|
||||
"Build the release now?",
|
||||
"Build the release now?",
|
||||
"\"build the release now\"",
|
||||
"`build the release now`",
|
||||
"> build the release now",
|
||||
"in the log it says build failed, what should we do",
|
||||
] {
|
||||
assert_eq!(operate_goal_from_prompt(prompt), None, "{prompt}");
|
||||
}
|
||||
for prompt in [
|
||||
"Build the release and verify the checksums",
|
||||
"Please fix the flaky CI test",
|
||||
"Fix 中文文档中的链接 and verify them",
|
||||
"run \"cargo build --release\" and verify the result",
|
||||
] {
|
||||
assert_eq!(
|
||||
operate_goal_from_prompt(prompt).expect(prompt).objective,
|
||||
prompt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operate_respects_goal_opt_out_including_negated_lists() {
|
||||
for prompt in [
|
||||
"Run one bounded cancellation check. Do not edit files, inspect other files, create a goal, spawn agents, or start any other tool.",
|
||||
"Build the example, but don't create a goal.",
|
||||
"Fix the button. Don’t create a persistent goal for this.",
|
||||
"Run the check without a goal.",
|
||||
"Verify the output; no goal tracking for this task.",
|
||||
"Update the sample. Never set a goal automatically.",
|
||||
"Fix the test. Do not use /goal.",
|
||||
"Run the check without creating a goal.",
|
||||
"Fix the example. Don't create any goals.",
|
||||
"Run one check. Do not edit files,\ncreate a goal, or spawn agents.",
|
||||
"Fix this issue, 不要创建目标。",
|
||||
] {
|
||||
assert_eq!(operate_goal_from_prompt(prompt), None, "{prompt}");
|
||||
}
|
||||
// A separate prohibition must not negate a later work instruction.
|
||||
let work = "Fix the failing checks. Do not publish. Build the release and verify it.";
|
||||
assert_eq!(operate_goal_from_prompt(work).unwrap().objective, work);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_goal_rejects_objective_knob_instead_of_ignoring_it() {
|
||||
// #5123-class: `objective` used to return a success receipt with no
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ Run `/mode` to open the mode picker, or switch directly with `/mode work`,
|
||||
|
||||
- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available.
|
||||
- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `bash`, `agent`, `workflow`, and `todo_write`, plus `create_goal`, `get_goal`, and `update_goal` so goal controls are available without discovery. Goals still require an explicit user request; approval, sandbox, repository law, and managed policy decide what may execute.
|
||||
- **Operate**: manage a goal through planned steps and verified results. Fleet configures the same sub-agents and roles that execute those steps. It has the same primitive identities and execution authority as Work. When no unfinished goal exists, a direct work instruction (not a greeting, acknowledgement, question, quoted command, conversational followup, or request declining goal tracking) becomes the session goal automatically, with continuation on; the transcript shows `◆ goal set · Operate keeps working until it is verified · /goal to edit`. An explicit `/goal` declaration still wins, `/goal` still edits it, and an existing goal is never replaced. The parent session is the **operator**: handle small or tightly coupled tasks directly. Before multi-step delegation, state a compact plan with named steps, dependencies, bounded file scopes and a completion check, then run it through the existing Workflow tool. Parallelize independent steps; each phase receives the previous phase's results, and a dependent step cannot start when a required result is missing. A single bounded independent task can use a direct `agent` call. Reuse a worker with followup for corrections and report completed, blocked and next steps. **Dispatch is not completion** — write-capable children must return real verification evidence. The first Operate turn of a session appends this contract once as a user-role runtime message (append-only history, never the pinned system prompt), so Plan, Work, and Operate keep one shared prompt prefix.
|
||||
- **Operate**: manage a goal through planned steps and verified results. Fleet configures the same sub-agents and roles that execute those steps. It has the same primitive identities and execution authority as Work. Goals are model-decided: the agent calls `create_goal` when a request is a durable objective, and `/goal` always works as the direct user control — the host never infers a goal from wording. Once a goal exists, the transcript shows `◆ goal set · Operate keeps working until it is verified · /goal to edit`. An explicit `/goal` declaration always wins, `/goal` still edits it, and an existing goal is never replaced. The parent session is the **operator**: handle small or tightly coupled tasks directly. Before multi-step delegation, state a compact plan with named steps, dependencies, bounded file scopes and a completion check, then run it through the existing Workflow tool. Parallelize independent steps; each phase receives the previous phase's results, and a dependent step cannot start when a required result is missing. A single bounded independent task can use a direct `agent` call. Reuse a worker with followup for corrections and report completed, blocked and next steps. **Dispatch is not completion** — write-capable children must return real verification evidence. The first Operate turn of a session appends this contract once as a user-role runtime message (append-only history, never the pinned system prompt), so Plan, Work, and Operate keep one shared prompt prefix.
|
||||
|
||||
`Act` and `/mode act` remain compatibility aliases for Work. Saved settings
|
||||
still normalize to the internal value `agent`.
|
||||
|
||||
Reference in new issue
Block a user