diff --git a/orchestrator_prompt.md b/orchestrator_prompt.md index a38bc40..7bef346 100644 --- a/orchestrator_prompt.md +++ b/orchestrator_prompt.md @@ -16,6 +16,9 @@ On a clean launch: 1. Run `multiagent workflow context "$MULTIAGENT_WORKFLOW_ID"`. 2. Read the authenticated task artifact named by `originalTask` exactly once. + Use only the exact writable path in `resultCandidate.path` for caller-result + handoff files; never write inside the workflow directory containing + `originalTask`. 3. Route from that typed context. Do not inspect panes, rediscover state paths, or reconstruct provider transcripts. @@ -94,11 +97,11 @@ bindings, independent review, and phase completion. request at `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json`. A prose proposal or `awaiting` report is not a result; restore that ops identity, then run a new reviewed cycle with a fresh reviewer. -- For successful external-only work, synthesize one self-contained caller - response from the original goal and all accumulated `opsResult` values. Write - it to `$MULTIAGENT_STATE_DIR/orchestrator-result.md`, then complete with - `multiagent orchestrator complete --external-only --result-file - "$MULTIAGENT_STATE_DIR/orchestrator-result.md"`. The runtime rejects external +- For successful or terminally blocked external-only work, synthesize one + self-contained caller response from the original goal and all accumulated + `opsResult` values. Write it to the exact `resultCandidate.path` returned by + `workflow context`, then complete with `multiagent orchestrator complete + --external-only --result-file RESULT_CANDIDATE_PATH`. The runtime rejects external completion without this bounded result handoff. Do not enter source lifecycle phases or pass a private agent artifact as the caller response. - Preserve literal predicates from the authenticated goal. When the caller diff --git a/prompts/playbooks/orchestration-routing.md b/prompts/playbooks/orchestration-routing.md index b0ca0b8..794114f 100644 --- a/prompts/playbooks/orchestration-routing.md +++ b/prompts/playbooks/orchestration-routing.md @@ -8,7 +8,8 @@ own role-specific procedure; this file does not repeat them. - Answer directly, or ask one bounded clarification, when the authenticated request can be handled from the current conversation without reading the repository, calling an external service, or producing an artifact. Persist - the exact response under `MULTIAGENT_STATE_DIR`, then request + the exact response at the `resultCandidate.path` returned by workflow context, + then request `multiagent orchestrator complete --direct-response --result-file PATH`. - Use a `reader` when answering requires repository inspection but no source mutation. Readers run in the repository working directory with mechanically diff --git a/prompts/playbooks/reviewed-ops-cycle.md b/prompts/playbooks/reviewed-ops-cycle.md index 5c2e816..5228380 100644 --- a/prompts/playbooks/reviewed-ops-cycle.md +++ b/prompts/playbooks/reviewed-ops-cycle.md @@ -90,13 +90,14 @@ response. It must include every caller-requested field and its supporting evidence, not merely a completion statement. A new caller-authorized session is required for more work. -For an external-only task with successful reviewed operations and no source -changes, write that caller response to -`$MULTIAGENT_STATE_DIR/orchestrator-result.md`, then finish with: +For an external-only task with successful reviewed operations, or a terminal +reviewed structural blocker, and no source changes, write that caller response +to the exact `resultCandidate.path` returned by workflow context, then finish +with: ```bash multiagent orchestrator complete --external-only \ - --result-file "$MULTIAGENT_STATE_DIR/orchestrator-result.md" + --result-file "RESULT_CANDIDATE_PATH" ``` The result artifact is the control server handoff, not a substitute for an diff --git a/runtime/src/workflow.rs b/runtime/src/workflow.rs index 188aa7d..2f2b18f 100644 --- a/runtime/src/workflow.rs +++ b/runtime/src/workflow.rs @@ -709,6 +709,7 @@ fn context(args: &[String]) -> Result<(), String> { .map_err(|error| format!("inspect original task artifact: {error}"))? .len(); let identities = typed_identity_context(&store.state_dir, MAX_IDENTITIES)?; + let result_candidate = store.state_dir.join("orchestrator-result-candidate.md"); let value = serde_json::json!({ "apiVersion": "multiagent.moveindustries.io/v1", "kind": "WorkflowContext", @@ -723,6 +724,11 @@ fn context(args: &[String]) -> Result<(), String> { "mediaType": "text/plain", "truncated": false }, + "resultCandidate": { + "path": result_candidate, + "mediaType": "text/plain", + "maxBytes": 6000 + }, "activeTodoCount": read_todos(&p.todos)?.iter().filter(|row| active(row.get(4))).count(), "reviewCount": read_reviews(&p.reviews)?.len(), "identities": identities @@ -1541,6 +1547,7 @@ pub fn supervisor_complete_external(id: &str) -> Result { let operations_dir = store.state_dir.join("operations"); let mut successful_operations = 0usize; let mut failed_operations = 0usize; + let mut blocked_operations = 0usize; if operations_dir.is_dir() { for entry in fs::read_dir(&operations_dir) .map_err(|error| format!("list external operation receipts: {error}"))? @@ -1581,9 +1588,8 @@ pub fn supervisor_complete_external(id: &str) -> Result { .and_then(serde_json::Value::as_str), ) { (Some("succeeded"), Some("succeeded")) => successful_operations += 1, - (Some("failed"), Some("failed")) | (Some("blocked"), Some("blocked")) => { - failed_operations += 1 - } + (Some("failed"), Some("failed")) => failed_operations += 1, + (Some("blocked"), Some("blocked")) => blocked_operations += 1, _ => { return Err(format!( "external-only completion requires consistently classified terminal receipts; {} has mismatched state and disposition", @@ -1593,10 +1599,9 @@ pub fn supervisor_complete_external(id: &str) -> Result { } } } - if successful_operations == 0 { + if successful_operations == 0 && (blocked_operations == 0 || failed_operations > 0) { return Err( - "external-only completion requires at least one successful reviewed operation receipt" - .into(), + "external-only completion requires a successful reviewed operation receipt or a terminal reviewed blocker without executor failures".into(), ); } crate::subagent::external_completion_gate_check()?; @@ -1610,7 +1615,7 @@ pub fn supervisor_complete_external(id: &str) -> Result { &p.events, "phase_transitioned", &format!( - "from=pre-implementation\tto=complete\titeration={}\tauthority=supervisor\troute=external-only\toperations={successful_operations}\tfailed_operations={failed_operations}", + "from=pre-implementation\tto=complete\titeration={}\tauthority=supervisor\troute=external-only\toperations={successful_operations}\tfailed_operations={failed_operations}\tblocked_operations={blocked_operations}", state_value(&state, "iteration") ), )?; diff --git a/tests/lifecycle.sh b/tests/lifecycle.sh index 2cdb12c..e467382 100755 --- a/tests/lifecycle.sh +++ b/tests/lifecycle.sh @@ -56,6 +56,7 @@ PROMPT_BUNDLE="$TEST_TMP/orchestrator-bundle.md" --output "$PROMPT_BUNDLE" >/dev/null assert_contains "$PROMPT_BUNDLE" "BEGIN ORCHESTRATION ROUTING CONTRACT" assert_contains "$PROMPT_BUNDLE" "--direct-response" +assert_contains "$PROMPT_BUNDLE" "resultCandidate.path" assert_contains "$PROMPT_BUNDLE" "BEGIN MANDATORY IMPLEMENTATION LIFECYCLE" assert_contains "$PROMPT_BUNDLE" "post-implementation -> pre-implementation" @@ -588,8 +589,46 @@ assert_contains "$EXTERNAL_STATE/workflows/WF-EXTERNAL/lifecycle/lifecycle.env" assert_contains "$EXTERNAL_STATE/workflows/WF-EXTERNAL/lifecycle/events.log" \ "route=external-only" assert_contains "$EXTERNAL_STATE/workflows/WF-EXTERNAL/lifecycle/events.log" \ - $'operations=1\tfailed_operations=3' + $'operations=1\tfailed_operations=2\tblocked_operations=1' assert_contains "$EXTERNAL_STATE/orchestrator-result.md" \ "External operation completed with reviewed evidence." +BLOCKED_EXTERNAL_STATE="$TEST_TMP/blocked-external-state" +mkdir -p "$BLOCKED_EXTERNAL_STATE/operations/OP-BLOCKED" +MULTIAGENT_STATE_DIR="$BLOCKED_EXTERNAL_STATE" \ + "$MULTIAGENT" workflow init WF-BLOCKED-EXTERNAL >/dev/null +cp "$EXTERNAL_STATE/operations/OP-BLOCKED/receipt.json" \ + "$BLOCKED_EXTERNAL_STATE/operations/OP-BLOCKED/receipt.json" +BLOCKED_EXTERNAL_RESULT="$BLOCKED_EXTERNAL_STATE/external-result-candidate.md" +printf 'The reviewed operation reached a terminal structural blocker.\n' \ + >"$BLOCKED_EXTERNAL_RESULT" +MULTIAGENT_ROOT="$EXTERNAL_ROOT" MULTIAGENT_STATE_DIR="$BLOCKED_EXTERNAL_STATE" \ + MULTIAGENT_WORKFLOW_ID=WF-BLOCKED-EXTERNAL MULTIAGENT_RUN_ID=RUN-BLOCKED-EXTERNAL \ + MULTIAGENT_LIFECYCLE_ENFORCEMENT=1 \ + "$MULTIAGENT" orchestrator complete --external-only \ + --result-file "$BLOCKED_EXTERNAL_RESULT" >"$TEST_TMP/blocked-external-complete.out" +assert_contains "$BLOCKED_EXTERNAL_STATE/workflows/WF-BLOCKED-EXTERNAL/lifecycle/events.log" \ + $'operations=0\tfailed_operations=0\tblocked_operations=1' +assert_contains "$BLOCKED_EXTERNAL_STATE/orchestrator-result.md" \ + "terminal structural blocker" + +FAILED_EXTERNAL_STATE="$TEST_TMP/failed-external-state" +mkdir -p "$FAILED_EXTERNAL_STATE/operations/OP-FAILED" +MULTIAGENT_STATE_DIR="$FAILED_EXTERNAL_STATE" \ + "$MULTIAGENT" workflow init WF-FAILED-EXTERNAL >/dev/null +cp "$EXTERNAL_STATE/operations/OP-FAILED/receipt.json" \ + "$FAILED_EXTERNAL_STATE/operations/OP-FAILED/receipt.json" +FAILED_EXTERNAL_RESULT="$FAILED_EXTERNAL_STATE/external-result-candidate.md" +printf 'The executor failed.\n' >"$FAILED_EXTERNAL_RESULT" +if MULTIAGENT_ROOT="$EXTERNAL_ROOT" MULTIAGENT_STATE_DIR="$FAILED_EXTERNAL_STATE" \ + MULTIAGENT_WORKFLOW_ID=WF-FAILED-EXTERNAL MULTIAGENT_RUN_ID=RUN-FAILED-EXTERNAL \ + MULTIAGENT_LIFECYCLE_ENFORCEMENT=1 \ + "$MULTIAGENT" orchestrator complete --external-only \ + --result-file "$FAILED_EXTERNAL_RESULT" >"$TEST_TMP/failed-external-complete.out" 2>&1; then + echo "expected executor failure without success or blocker to reject completion" >&2 + exit 1 +fi +assert_contains "$TEST_TMP/failed-external-complete.out" \ + "terminal reviewed blocker without executor failures" + echo "implementation lifecycle tests passed"