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
73 changes: 65 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,23 @@ the artifacts behind each transition.

```mermaid
flowchart TD
User["User task"] --> Orchestrator["Orchestrator CLI"]
Orchestrator --> DAG["Assignments and dependency DAG"]
User["User task"] --> Pre["Pre-implementation"]
Pre --> Authority["Independent authority review"]
Authority --> Choice{"User-owned decision?"}
Choice -- "yes" --> UserDecision["Ask user and record choice"]
Choice -- "no" --> Context["Approved implementation context"]
UserDecision --> Context
Context --> DAG["Assignments and dependency DAG"]
DAG --> WorkerA["Worker A"]
DAG --> WorkerB["Worker B"]
WorkerA --> Repo["Target Git repository"]
WorkerB --> Repo
Repo --> Snapshot["Exact diff snapshot"]
Snapshot --> Verifier["Independent verifier"]
Verifier --> Findings["Findings, todos, and recheck evidence"]
Findings --> Gate{"Final gate"}
Gate -- "reject and repair" --> Orchestrator
Snapshot --> Reviews["Post-implementation reviews"]
Reviews --> Findings["Findings, todos, and recheck evidence"]
Findings --> Todo{"Active TODO?"}
Todo -- "yes" --> Pre
Todo -- "no" --> Gate{"Lifecycle and technical gates"}
Gate -- "hash-bound evidence passes" --> Result["Accepted patch"]
```

Expand All @@ -77,6 +83,43 @@ Launches are clean by default. Explicit crash recovery is opt-in:
./launch.sh --resume --session multiagent --root /absolute/path/to/target-repo
```

## Implementation Lifecycle

`launch.sh` bundles the orchestrator role with the mandatory lifecycle prompt,
records prompt hashes, and initializes durable lifecycle state under:

```text
$MULTIAGENT_STATE_DIR/workflows/$MULTIAGENT_WORKFLOW_ID/lifecycle/
```

`bin/workflow.sh` is the shell entry point; the importable lifecycle state
machine and CLI implementation live in `multiagent_framework/workflow.py`.

The enforced normal path is `pre-implementation -> implementation ->
post-implementation`. An independent authority review identifies consequential
choices and whether the user or orchestrator owns each one. Writable workers
receive the complete approved implementation context, not only a partial
assignment summary. Any accepted review finding creates a TODO and returns through
pre-implementation before another edit iteration.
The implementation permit also verifies that `bin/decision.sh` contains a
committed decision whose selected plan matches the context and assignment.

Inspect and advance the state with:

```bash
bin/workflow.sh status "$MULTIAGENT_WORKFLOW_ID"
bin/workflow.sh prepare-implementation "$MULTIAGENT_WORKFLOW_ID" \
--decision-id DECISION_ID --plan-id PLAN_ID --decision-revision REVISION \
--implementation-context CONTEXT_PATH --authority-review REVIEW_ID
bin/workflow.sh transition "$MULTIAGENT_WORKFLOW_ID" implementation
bin/workflow.sh completion-check "$MULTIAGENT_WORKFLOW_ID"
```

`MULTIAGENT_LIFECYCLE_ENFORCEMENT=1` is the default. Existing structured
technical findings and repair TODOs remain authoritative. Running
`bin/orchestrator.sh complete` requires both the lifecycle completion gate and
`bin/subagent.sh gate-check`.

The default roles use Codex for orchestration and verification and Claude for
workers. `WORKER_CLI`: worker CLI for manual worker windows, default `claude`.
`VERIFIER_CLI`: verifier CLI, default `codex`. CLI choices, recovery, ownership
Expand All @@ -94,8 +137,8 @@ contracts and workflows:
`hidden-contract-ledger`, and hidden-contract edge cases;
- **Scope Guard Workflow**, **Validation Coordinator Workflow**, the validation lease table,
`validation-run`, and `validation-lease-acquire`;
- **Verifier Workflow**, its compact contract ledger, and
`MULTIAGENT_VERIFIER_MAX_ITERATIONS=3`;
- **Verifier Workflow**, its compact contract ledger, and the
`MULTIAGENT_VERIFIER_MAX_ITERATIONS=3` escalation threshold;
- Codex UI dashboard watching through `bin/watch.sh`, backed by tmux pane logs
under `.multiagent/logs`, blocked-agent state, and workflow DAG nodes;
- preflight checks that prevent a scaffold, shim, or proxy behavior from being
Expand Down Expand Up @@ -135,3 +178,17 @@ an advanced path.
```bash
tests/run.sh
```

## Enforcement Caveat

Decision-authority review, approved-context handoff, lifecycle TODO convergence,
and completion are enforced by the orchestrator prompt plus normal-path checks
in `bin/workflow.sh`, `bin/subagent.sh`, and `bin/orchestrator.sh`. This makes
ordinary violations fail visibly, but it is not a security or capability
boundary: an orchestrator with direct shell and state-file access can bypass or
disable these checks.

Revisit this limitation before treating the workflow as strict enforcement.
The stronger design is a trusted supervisor that exclusively owns writable
worker launch and independently validates TODO state, decision ownership, user
approval, context revision, and assignment scope before starting a worker.
51 changes: 51 additions & 0 deletions bin/orchestrator.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="${MULTIAGENT_ROOT:-$(pwd)}"
STATE_DIR="${MULTIAGENT_STATE_DIR:-$ROOT/.multiagent}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"

usage() {
cat <<'USAGE'
Usage:
bin/orchestrator.sh complete

Runs the normal-path completion gates for the active orchestrated workflow.
USAGE
}

complete_run() {
if [[ "${MULTIAGENT_LIFECYCLE_ENFORCEMENT:-0}" == "1" ]]; then
local workflow_id="${MULTIAGENT_WORKFLOW_ID:-}"
[[ -n "$workflow_id" ]] || {
echo "orchestrator: lifecycle enforcement requires MULTIAGENT_WORKFLOW_ID" >&2
exit 1
}
MULTIAGENT_STATE_DIR="$STATE_DIR" "$SCRIPT_DIR/workflow.sh" completion-check "$workflow_id" >/dev/null
local phase
phase="$(MULTIAGENT_STATE_DIR="$STATE_DIR" "$SCRIPT_DIR/workflow.sh" value "$workflow_id" phase)"
if [[ "$phase" != "complete" ]]; then
echo "orchestrator: workflow must transition to complete before run completion (current: $phase)" >&2
exit 1
fi
fi

MULTIAGENT_ROOT="$ROOT" MULTIAGENT_STATE_DIR="$STATE_DIR" "$SCRIPT_DIR/subagent.sh" gate-check >/dev/null
printf 'run completed\t%s\n' "${MULTIAGENT_RUN_ID:-${MULTIAGENT_WORKFLOW_ID:-unknown}}"
}

case "${1:-}" in
complete)
shift
[[ $# -eq 0 ]] || { usage >&2; exit 2; }
complete_run
;;
-h|--help|"")
usage
;;
*)
echo "orchestrator: unknown command: $1" >&2
usage >&2
exit 2
;;
esac
63 changes: 63 additions & 0 deletions bin/prompt-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<'USAGE'
Usage:
bin/prompt-bundle.sh --orchestrator PATH --lifecycle PATH --output PATH

Builds the canonical initial orchestrator prompt from the role prompt and the
mandatory implementation lifecycle playbook.
USAGE
}

die() {
echo "prompt-bundle: $*" >&2
exit 1
}

orchestrator=""
lifecycle=""
output=""
while [[ $# -gt 0 ]]; do
case "$1" in
--orchestrator)
orchestrator="${2:-}"
shift 2
;;
--lifecycle)
lifecycle="${2:-}"
shift 2
;;
--output)
output="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done

[[ -f "$orchestrator" ]] || die "orchestrator prompt not found: $orchestrator"
[[ -f "$lifecycle" ]] || die "lifecycle prompt not found: $lifecycle"
[[ -n "$output" ]] || die "--output is required"

mkdir -p "$(dirname "$output")"
tmp="$(mktemp "$(dirname "$output")/.orchestrator-prompt.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
{
printf '%s\n\n' '----- BEGIN ORCHESTRATOR ROLE -----'
cat "$orchestrator"
printf '\n%s\n\n' '----- END ORCHESTRATOR ROLE -----'
printf '%s\n\n' '----- BEGIN MANDATORY IMPLEMENTATION LIFECYCLE -----'
cat "$lifecycle"
printf '\n%s\n' '----- END MANDATORY IMPLEMENTATION LIFECYCLE -----'
} >"$tmp"
mv "$tmp" "$output"
trap - EXIT
printf 'prompt bundle built\t%s\n' "$output"
88 changes: 81 additions & 7 deletions bin/subagent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ ROOT="${MULTIAGENT_ROOT:-$(pwd)}"
STATE_DIR="${MULTIAGENT_STATE_DIR:-$ROOT/.multiagent}"
LOG_DIR="${MULTIAGENT_LOG_DIR:-$STATE_DIR/logs}"
POLICY_FILE="${MULTIAGENT_WRITE_POLICY:-$ROOT/docs/write-policy.paths}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
CODEX_BIN="${CODEX_BIN:-codex}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
WORKER_CLI="${WORKER_CLI:-claude}"
SUBAGENT_CLI="${SUBAGENT_CLI:-$WORKER_CLI}"
VERIFIER_CLI="${VERIFIER_CLI:-codex}"
MULTIAGENT_HELPER="${MULTIAGENT_HELPER:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")}"
MULTIAGENT_HELPER="${MULTIAGENT_HELPER:-$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")}"
MULTIAGENT_REQUIRE_HASH_BOUND_VERIFIER="${MULTIAGENT_REQUIRE_HASH_BOUND_VERIFIER:-1}"
PROMPT_MODULE_ROOT="${MULTIAGENT_PROMPT_MODULE_ROOT:-$ROOT}"
FRAMEWORK_MODULE_ROOT="${MULTIAGENT_FRAMEWORK_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
Expand Down Expand Up @@ -172,7 +173,9 @@ role_prompt_path() {
local role="$2"
local lower_name
lower_name="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')"
if [[ "$lower_name" == *build-verifier* ]]; then
if [[ "$lower_name" == *decision-authority-reviewer* ]]; then
printf '%s\n' "$PROMPT_MODULE_ROOT/prompts/roles/decision-authority-reviewer.md"
elif [[ "$lower_name" == *build-verifier* ]]; then
printf '%s\n' "$PROMPT_MODULE_ROOT/prompts/roles/build-verifier.md"
elif [[ "$role" == "verifier" || "$role" == "reviewer" || "$lower_name" == *verifier* || "$lower_name" == *review* ]]; then
printf '%s\n' "$PROMPT_MODULE_ROOT/prompts/verifier.md"
Expand Down Expand Up @@ -537,6 +540,50 @@ get_assignment_status() {
fi
}

lifecycle_enforced() {
[[ "${MULTIAGENT_LIFECYCLE_ENFORCEMENT:-0}" == "1" ]]
}

workflow_value() {
local workflow_id="$1"
local key="$2"
"$SCRIPT_DIR/workflow.sh" value "$workflow_id" "$key"
}

gate_implementation_assignment() {
local workflow_id="$1"
local decision_id="$2"
local plan_id="$3"
[[ -x "$SCRIPT_DIR/workflow.sh" ]] || die "missing lifecycle helper: $SCRIPT_DIR/workflow.sh"
[[ -n "$workflow_id" ]] || die "lifecycle enforcement requires --workflow-id for exploitation assignments"
[[ -n "$decision_id" ]] || die "lifecycle enforcement requires --decision-id for exploitation assignments"
[[ -n "$plan_id" ]] || die "lifecycle enforcement requires --plan-id for exploitation assignments"
"$SCRIPT_DIR/workflow.sh" gate "$workflow_id" implementation \
--decision-id "$decision_id" --plan-id "$plan_id" >/dev/null || \
die "workflow implementation gate rejected assignment for workflow $workflow_id"
}

validated_assignment_context_path() {
local name="$1"
lifecycle_enforced || return 1
[[ -f "$(assignment_meta_file "$name")" ]] || return 1

local role workflow_id decision_id plan_id assignment_revision current_revision context_path
role="$(read_assignment_value "$name" role || true)"
[[ "$role" == "exploitation" ]] || return 1
workflow_id="$(read_assignment_value "$name" workflow_id || true)"
decision_id="$(read_assignment_value "$name" decision_id || true)"
plan_id="$(read_assignment_value "$name" plan_id || true)"
gate_implementation_assignment "$workflow_id" "$decision_id" "$plan_id"
assignment_revision="$(read_assignment_value "$name" decision_revision || true)"
current_revision="$(workflow_value "$workflow_id" decision_revision)"
[[ -n "$assignment_revision" && "$assignment_revision" == "$current_revision" ]] || \
die "assignment decision revision is stale: assignment=${assignment_revision:-missing} workflow=$current_revision"
context_path="$(read_assignment_value "$name" implementation_context || true)"
[[ -f "$context_path" ]] || die "assignment approved implementation context is missing: $context_path"
printf '%s\n' "$context_path"
}

status_is_active_worker() {
local status="$1"
case "$status" in
Expand Down Expand Up @@ -748,14 +795,23 @@ assignment_create() {
die "invalid role '$role' (expected exploitation|exploration|reflection|architecture|qa|verifier|scout)"
;;
esac
if [[ "$role" == "exploitation" ]] && lifecycle_enforced; then
[[ -n "$workflow_id" ]] || workflow_id="${MULTIAGENT_WORKFLOW_ID:-}"
gate_implementation_assignment "$workflow_id" "$decision_id" "$plan_id"
fi
if [[ -z "$start_commit" ]]; then
start_commit="$(git -C "$ROOT" rev-parse HEAD)"
else
git -C "$ROOT" rev-parse --verify "$start_commit^{commit}" >/dev/null || die "invalid start commit: $start_commit"
start_commit="$(git -C "$ROOT" rev-parse "$start_commit^{commit}")"
fi

local dir owned_file item normalized
local dir owned_file item normalized decision_revision="" implementation_context="" implementation_context_sha256=""
if [[ "$role" == "exploitation" ]] && lifecycle_enforced; then
decision_revision="$(workflow_value "$workflow_id" decision_revision)"
implementation_context="$(workflow_value "$workflow_id" implementation_context)"
implementation_context_sha256="$(workflow_value "$workflow_id" implementation_context_sha256)"
fi
dir="$(assignment_dir "$name")"
mkdir -p "$dir"
owned_file="$(assignment_owned_file "$name")"
Expand Down Expand Up @@ -784,6 +840,9 @@ verifier_cli=$VERIFIER_CLI
role=$role
decision_id=$decision_id
plan_id=$plan_id
decision_revision=$decision_revision
implementation_context=$implementation_context
implementation_context_sha256=$implementation_context_sha256
workflow_id=$workflow_id
node_id=$node_id
depends_on=$depends_on
Expand Down Expand Up @@ -1303,6 +1362,16 @@ spawn_subagent() {
fi
fi

local implementation_context_path=""
if implementation_context_path="$(validated_assignment_context_path "$name")"; then
[[ -n "$instruction_file" ]] || \
die "lifecycle-enforced exploitation spawn requires --instruction-file with the complete approved implementation context"
local required_context
required_context="$(cat "$implementation_context_path")"
[[ -n "$required_context" && "$instruction" == *"$required_context"* ]] || \
die "exploitation instruction does not contain the complete approved implementation context"
fi

local dir
dir="$(subagent_dir "$name")"
mkdir -p "$dir" "$LOG_DIR"
Expand Down Expand Up @@ -1333,8 +1402,8 @@ EOF
cat "$prompt_file"
} >>"$dir/transcript.log"
fi
printf -v command "cd %q && export MULTIAGENT_SESSION=%q MULTIAGENT_ROOT=%q MULTIAGENT_STATE_DIR=%q MULTIAGENT_LOG_DIR=%q MULTIAGENT_WRITE_POLICY=%q MULTIAGENT_SUBAGENT_NAME=%q MULTIAGENT_HELPER=%q WORKER_CLI=%q SUBAGENT_CLI=%q VERIFIER_CLI=%q CODEX_BIN=%q CLAUDE_BIN=%q MULTIAGENT_CODEX_EXEC=%q PATH=%q && %s; rc=\$?; printf '\\nfinal status: codex exec exited rc=%%s\\n' \$rc; sleep infinity" \
"$ROOT" "$SESSION" "$ROOT" "$STATE_DIR" "$LOG_DIR" "$POLICY_FILE" "$name" "$MULTIAGENT_HELPER" "$WORKER_CLI" "$cli" "$VERIFIER_CLI" "$CODEX_BIN" "$CLAUDE_BIN" "${MULTIAGENT_CODEX_EXEC:-0}" "$PATH" "$(build_cli_command "$cli" "$ROOT" "$prompt_file" "$output_file")"
printf -v command "cd %q && export MULTIAGENT_SESSION=%q MULTIAGENT_ROOT=%q MULTIAGENT_STATE_DIR=%q MULTIAGENT_LOG_DIR=%q MULTIAGENT_WRITE_POLICY=%q MULTIAGENT_WORKFLOW_ID=%q MULTIAGENT_LIFECYCLE_ENFORCEMENT=%q MULTIAGENT_SUBAGENT_NAME=%q MULTIAGENT_HELPER=%q WORKER_CLI=%q SUBAGENT_CLI=%q VERIFIER_CLI=%q CODEX_BIN=%q CLAUDE_BIN=%q MULTIAGENT_CODEX_EXEC=%q PATH=%q && %s; rc=\$?; printf '\\nfinal status: codex exec exited rc=%%s\\n' \$rc; sleep infinity" \
"$ROOT" "$SESSION" "$ROOT" "$STATE_DIR" "$LOG_DIR" "$POLICY_FILE" "${MULTIAGENT_WORKFLOW_ID:-}" "${MULTIAGENT_LIFECYCLE_ENFORCEMENT:-0}" "$name" "$MULTIAGENT_HELPER" "$WORKER_CLI" "$cli" "$VERIFIER_CLI" "$CODEX_BIN" "$CLAUDE_BIN" "${MULTIAGENT_CODEX_EXEC:-0}" "$PATH" "$(build_cli_command "$cli" "$ROOT" "$prompt_file" "$output_file")"
tmux new-window -d -t "$SESSION" -n "$name" "$command"
pipe_log "$name"
set_status "$name" "running"
Expand Down Expand Up @@ -1584,6 +1653,11 @@ restore_subagent() {

local instruction command
instruction="$(restore_instruction "$name" "$prior_status" "$dir")"
local implementation_context_path=""
if implementation_context_path="$(validated_assignment_context_path "$name")"; then
instruction+=$'\n\n## Approved Implementation Context\n\n'
instruction+="$(cat "$implementation_context_path")"
fi
printf '%s\n' "$(timestamp) prior_status=$prior_status action=$action reason=$reason force=$force cli=$cli" >>"$dir/restore_events.log"
{
printf '\n----- restore seed %s -----\n' "$(timestamp)"
Expand All @@ -1599,8 +1673,8 @@ restore_subagent() {
prompt_file="$dir/restore-instruction.txt"
printf '%s\n' "$instruction" >"$prompt_file"
fi
printf -v command "cd %q && export MULTIAGENT_SESSION=%q MULTIAGENT_ROOT=%q MULTIAGENT_STATE_DIR=%q MULTIAGENT_LOG_DIR=%q MULTIAGENT_WRITE_POLICY=%q MULTIAGENT_SUBAGENT_NAME=%q MULTIAGENT_HELPER=%q MULTIAGENT_SUBAGENT_RESTORED=1 WORKER_CLI=%q SUBAGENT_CLI=%q VERIFIER_CLI=%q CODEX_BIN=%q CLAUDE_BIN=%q MULTIAGENT_CODEX_EXEC=%q PATH=%q && %s; rc=\$?; printf '\\nfinal status: codex exec exited rc=%%s\\n' \$rc; sleep infinity" \
"$ROOT" "$SESSION" "$ROOT" "$STATE_DIR" "$LOG_DIR" "$POLICY_FILE" "$name" "$MULTIAGENT_HELPER" "$WORKER_CLI" "$cli" "$VERIFIER_CLI" "$CODEX_BIN" "$CLAUDE_BIN" "${MULTIAGENT_CODEX_EXEC:-0}" "$PATH" "$(build_cli_command "$cli" "$ROOT" "$prompt_file" "$output_file")"
printf -v command "cd %q && export MULTIAGENT_SESSION=%q MULTIAGENT_ROOT=%q MULTIAGENT_STATE_DIR=%q MULTIAGENT_LOG_DIR=%q MULTIAGENT_WRITE_POLICY=%q MULTIAGENT_WORKFLOW_ID=%q MULTIAGENT_LIFECYCLE_ENFORCEMENT=%q MULTIAGENT_SUBAGENT_NAME=%q MULTIAGENT_HELPER=%q MULTIAGENT_SUBAGENT_RESTORED=1 WORKER_CLI=%q SUBAGENT_CLI=%q VERIFIER_CLI=%q CODEX_BIN=%q CLAUDE_BIN=%q MULTIAGENT_CODEX_EXEC=%q PATH=%q && %s; rc=\$?; printf '\\nfinal status: codex exec exited rc=%%s\\n' \$rc; sleep infinity" \
"$ROOT" "$SESSION" "$ROOT" "$STATE_DIR" "$LOG_DIR" "$POLICY_FILE" "${MULTIAGENT_WORKFLOW_ID:-}" "${MULTIAGENT_LIFECYCLE_ENFORCEMENT:-0}" "$name" "$MULTIAGENT_HELPER" "$WORKER_CLI" "$cli" "$VERIFIER_CLI" "$CODEX_BIN" "$CLAUDE_BIN" "${MULTIAGENT_CODEX_EXEC:-0}" "$PATH" "$(build_cli_command "$cli" "$ROOT" "$prompt_file" "$output_file")"
tmux new-window -d -t "$SESSION" -n "$name" "$command"
pipe_log "$name"
set_status "$name" "running"
Expand Down
13 changes: 13 additions & 0 deletions bin/workflow.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="${MULTIAGENT_ROOT:-$(pwd)}"
STATE_DIR="${MULTIAGENT_STATE_DIR:-$ROOT/.multiagent}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
FRAMEWORK_ROOT="${MULTIAGENT_FRAMEWORK_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd -P)}"

export MULTIAGENT_ROOT="$ROOT"
export MULTIAGENT_STATE_DIR="$STATE_DIR"
export PYTHONPATH="$FRAMEWORK_ROOT${PYTHONPATH:+:$PYTHONPATH}"

exec python3 -m multiagent_framework.workflow "$@"
Loading
Loading