Skip to content
Draft
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
21 changes: 13 additions & 8 deletions .agent/docs/architecture/supported-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,11 @@ workflow accepts a pull request number, confirms the target is an open PR, and
requires latest trusted review synthesis from the authenticated Sepo actor for
the current reviewed-head marker before it runs an approval agent. Normal runs
require that synthesis to be `SHIP`; orchestrated review `HUMAN_DECISION`
handoffs may also run the agent as a decision gate for non-`SHIP` verdicts. The
agent runs with read-approved permissions and returns structured JSON with a
verdict, reason, optional follow-up context, and `inspected_head_sha`.
handoffs may also run the agent as a decision gate for non-`SHIP` verdicts, and
orchestrated `MINOR_ISSUES` reviews that recommend `NO_AUTOMATED_ACTION` may run
the gate when no required branch-change work remains. The agent runs with
read-approved permissions and returns structured JSON with a verdict, reason,
optional follow-up context, and `inspected_head_sha`.

Deterministic resolver code is the only part that can submit or record the
approval. It rereads the current PR head, rechecks trusted current-head review
Expand All @@ -299,13 +301,16 @@ unless both `AGENT_ALLOW_SELF_APPROVE=true` and `AGENT_ALLOW_SELF_MERGE=true`
are enabled, parses the agent verdict, and approves only when the expected,
current, and inspected head SHAs match. Normal handoffs require trusted
current-head `SHIP` review synthesis; orchestrated review `HUMAN_DECISION`
handoffs also trust the matching current-head synthesis as the decision gate.
Non-approval outcomes post a compact PR status comment. In full
handoffs also trust the matching current-head synthesis as the decision gate,
and `MINOR_ISSUES` plus `NO_AUTOMATED_ACTION` is trusted only when the matching
synthesis action items show no required branch-change work. Non-approval
outcomes post a compact PR status comment. In full
self-governance mode, same-actor approvals are recorded as a current-head
self-approval status comment rather than a GitHub review approval. In
orchestrated chains, `SHIP` review synthesis and review syntheses that recommend
`HUMAN_DECISION` can hand off to `agent-self-approve`; non-`SHIP`
`HUMAN_DECISION` runs let self-approval approve, request changes, or block. A
orchestrated chains, `SHIP` review synthesis, review syntheses that recommend
`HUMAN_DECISION`, and no-required-branch-work `MINOR_ISSUES` syntheses that
recommend `NO_AUTOMATED_ACTION` can hand off to `agent-self-approve`; non-`SHIP`
handoffs let self-approval approve, request changes, or block. A
self-approval `REQUEST_CHANGES` result can hand off to `fix-pr` with the
approval agent's handoff context. Self-approval status comments are upserted by
marker against comments authored by the authenticated Sepo actor, and result
Expand Down
9 changes: 5 additions & 4 deletions .agent/docs/technical-details/agent-orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ stateDiagram-v2
Implement --> Review: success + PR created
Implement --> Stop: failed or no PR

Review --> SelfApprove: SHIP or HUMAN_DECISION + AGENT_ALLOW_SELF_APPROVE=true
Review --> FixPR: MINOR_ISSUES / NEEDS_REWORK / CHANGES_REQUESTED without HUMAN_DECISION
Review --> Stop: SHIP or HUMAN_DECISION + self-approval disabled
Review --> SelfApprove: SHIP / HUMAN_DECISION / MINOR_ISSUES no required branch work + AGENT_ALLOW_SELF_APPROVE=true
Review --> FixPR: MINOR_ISSUES / NEEDS_REWORK / CHANGES_REQUESTED with required branch work
Review --> Stop: SHIP / HUMAN_DECISION / no required branch work + self-approval disabled
Review --> Stop: failed or unsupported verdict

SelfApprove --> FixPR: REQUEST_CHANGES
Expand Down Expand Up @@ -67,6 +67,7 @@ When an action-originated handoff is used, the orchestrator also accepts:
- source action
- source conclusion
- source recommended next step, when the source is review synthesis
- whether the source review synthesis has required branch-change action items
- target issue or pull request number
- next target number when implementation opened a pull request
- source workflow run ID for duplicate-dispatch detection
Expand Down Expand Up @@ -176,7 +177,7 @@ envelopes use the planner path when enabled.

In `heuristics` mode, action-originated handoff decisions still use the fixed transition policy and round budget checks.

Review-originated `fix-pr` handoffs carry explicit task context when available. The review dispatcher derives it from the latest review synthesis action items, and heuristic mode falls back to a conservative instruction to address only unresolved review synthesis action items while ignoring optional INFO notes and metadata-only polish. When a review synthesis recommends `HUMAN_DECISION`, self-approval-enabled orchestration routes to `agent-self-approve` instead of `fix-pr` or a human stop; self-approval then decides whether to approve, request changes, or block. Manual PR `/orchestrate` starts with a `CHANGES_REQUESTED` review decision use separate context that tells `fix-pr` to address the latest unresolved requested-change review comments instead of the review-synthesis fallback. Self-approval `REQUEST_CHANGES` handoffs preserve the approval agent's handoff context as the `fix-pr` task. Self-approval `APPROVED` handoffs dispatch `agent-self-merge` only when `AGENT_ALLOW_SELF_MERGE=true`.
Review-originated `fix-pr` handoffs carry explicit task context when available. The review dispatcher derives it from the latest review synthesis action items, ignores sentinel no-op items such as "No required branch-change work remains", and heuristic mode falls back to a conservative instruction to address only unresolved review synthesis action items while ignoring optional INFO notes and metadata-only polish. When a review synthesis recommends `HUMAN_DECISION`, self-approval-enabled orchestration routes to `agent-self-approve` instead of `fix-pr` or a human stop; self-approval then decides whether to approve, request changes, or block. `NO_AUTOMATED_ACTION` from review means no more branch mutation/review loop is useful, not necessarily no approval gate: when the current review verdict is `MINOR_ISSUES`, no required branch-change work remains, and `AGENT_ALLOW_SELF_APPROVE=true`, orchestration may hand off to `agent-self-approve`. If self-approval is disabled, the orchestrator stops cleanly, and blocking verdicts or required branch-change action items still prevent that shortcut. Manual PR `/orchestrate` starts with a `CHANGES_REQUESTED` review decision use separate context that tells `fix-pr` to address the latest unresolved requested-change review comments instead of the review-synthesis fallback. Self-approval `REQUEST_CHANGES` handoffs preserve the approval agent's handoff context as the `fix-pr` task. Self-approval `APPROVED` handoffs dispatch `agent-self-merge` only when `AGENT_ALLOW_SELF_MERGE=true`.

In `agent` mode, the orchestrator first runs a scoped planner prompt through the same resolved-provider runtime used by other agent actions. The planner has its own `orchestrator` route and `planner` lane, so session continuation is separate from implement, review, and fix-pr sessions. The planner runs with `approve-all` tool permission so it can gather current GitHub and repository context in non-interactive workflows. It still receives read-only repository memory, selected read-only rubrics, the handoff envelope, any source handoff context, and original request, and returns JSON describing whether to stop, block, delegate a child issue, or hand off. For blocked decisions, the planner may return `user_message` or `clarification_request` to ask for missing context in the visible stop comment. For handoffs, the planner may also return `handoff_context`: explicit, action-oriented instructions for the next workflow. When the next action is `fix-pr`, the dispatcher passes that context into `agent-fix-pr.yml`, and the fix-pr prompt treats it as the selected task and constraints for the automated fix pass. The workflow uses the runtime preflight CLI to skip this planner when the max-round budget is already exhausted or the initial requester lacks delegated-route capability, and the runtime still validates planner JSON against the fixed transition policy, the issue-only direct-implement rule, and max-round budget before dispatching anything.

Expand Down
62 changes: 62 additions & 0 deletions .agent/src/__tests__/dispatch-agent-orchestrator-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,68 @@ exit 1
const payload = JSON.parse(readFileSync(payloadPath, "utf8"));
assert.equal(payload.inputs.source_conclusion, "minor_issues");
assert.equal(payload.inputs.source_recommended_next_step, "human_decision");
assert.equal(payload.inputs.source_required_branch_work, "true");
assert.equal(payload.inputs.source_handoff_context, "");
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});

test("dispatch-agent-orchestrator forwards no required branch-work review state", () => {
const tempDir = mkdtempSync(join(tmpdir(), "agent-dispatch-orchestrator-"));
try {
const payloadPath = join(tempDir, "dispatch.json");
const responsePath = join(tempDir, "response.md");
writeFileSync(
responsePath,
[
"## Recommended Next Step",
"NO_AUTOMATED_ACTION: no unresolved actionable work remains.",
"",
"## Final Verdict",
"MINOR_ISSUES",
"",
"## Action Items",
"- [ ] No required branch-change work remains.",
].join("\n"),
"utf8",
);
writeFileSync(join(tempDir, "gh"), `#!/usr/bin/env bash
set -euo pipefail
if [ "\${1-}" = "api" ] && [ "\${2-}" = "-X" ] && [ "\${3-}" = "POST" ]; then
cat > "$FAKE_DISPATCH_PAYLOAD"
exit 0
fi
printf 'unexpected gh args: %s\\n' "$*" >&2
exit 1
`, { encoding: "utf8", mode: 0o755 });

const result = spawnSync("node", [".agent/dist/cli/dispatch-agent-orchestrator.js"], {
cwd: repoRoot,
env: {
...process.env,
PATH: `${tempDir}:${process.env.PATH || ""}`,
FAKE_DISPATCH_PAYLOAD: payloadPath,
GITHUB_REPOSITORY: "self-evolving/repo",
DEFAULT_BRANCH: "main",
SOURCE_ACTION: "review",
RESPONSE_FILE: responsePath,
TARGET_KIND: "pull_request",
TARGET_NUMBER: "30",
REQUESTED_BY: "lolipopshock",
REQUEST_TEXT: "@sepo-agent /orchestrate",
AUTOMATION_MODE: "heuristics",
ORCHESTRATION_ENABLED: "true",
},
encoding: "utf8",
});

assert.equal(result.status, 0, result.stderr || result.stdout);
assert.ok(existsSync(payloadPath));
const payload = JSON.parse(readFileSync(payloadPath, "utf8"));
assert.equal(payload.inputs.source_conclusion, "minor_issues");
assert.equal(payload.inputs.source_recommended_next_step, "no_automated_action");
assert.equal(payload.inputs.source_required_branch_work, "false");
assert.equal(payload.inputs.source_handoff_context, "");
} finally {
rmSync(tempDir, { recursive: true, force: true });
Expand Down
10 changes: 10 additions & 0 deletions .agent/src/__tests__/envelope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1287,9 +1287,12 @@ test("execution workflows expose automation handoff inputs", () => {
assert.match(orchestratorWorkflow, /base_branch:/);
assert.match(orchestratorWorkflow, /base_pr:/);
assert.match(orchestratorWorkflow, /source_handoff_context:/);
assert.match(orchestratorWorkflow, /source_required_branch_work:/);
assert.match(orchestratorWorkflow, /AGENT_COLLAPSE_OLD_REVIEWS:\s*\$\{\{ vars\.AGENT_COLLAPSE_OLD_REVIEWS \}\}/);
assert.match(orchestratorWorkflow, /BASE_BRANCH:\s*\$\{\{ inputs\.base_branch \}\}/);
assert.match(orchestratorWorkflow, /SOURCE_REQUIRED_BRANCH_WORK:\s*\$\{\{ inputs\.source_required_branch_work \}\}/);
assert.match(orchestratorWorkflow, /SOURCE_HANDOFF_CONTEXT:\s*\$\{\{ inputs\.source_handoff_context \}\}/);
assert.match(orchestratorWorkflow, /ORCHESTRATOR_SOURCE_REQUIRED_BRANCH_WORK:\s*\$\{\{ inputs\.source_required_branch_work \}\}/);
assert.match(orchestratorWorkflow, /ORCHESTRATOR_SOURCE_HANDOFF_CONTEXT:\s*\$\{\{ inputs\.source_handoff_context \}\}/);
assert.match(orchestrateHandoffCli, /resolveEffectiveBaseInputs/);
assert.match(orchestrateHandoffCli, /baseBranch:\s*decision\.baseBranch \|\| baseBranch/);
Expand All @@ -1313,6 +1316,7 @@ test("execution workflows expose automation handoff inputs", () => {
assert.match(fixPrPrompt, /\$\{ORCHESTRATOR_CONTEXT\}/);
assert.match(orchestratorPrompt, /"handoff_context"/);
assert.match(orchestratorPrompt, /ORCHESTRATOR_SOURCE_HANDOFF_CONTEXT/);
assert.match(orchestratorPrompt, /ORCHESTRATOR_SOURCE_REQUIRED_BRANCH_WORK/);
assert.match(orchestratorPrompt, /ORCHESTRATOR_SELF_APPROVE_ENABLED/);
assert.match(orchestratorPrompt, /ORCHESTRATOR_SELF_MERGE_ENABLED/);
assert.match(orchestratorPrompt, /"user_message"/);
Expand All @@ -1335,12 +1339,18 @@ test("orchestrator source handoff context is renderable in planner prompts", ()
const runSource = readRepoFile(".agent/src/run.ts");
const orchestratorPrompt = readRepoFile(".github/prompts/agent-orchestrator.md");
const sourceContextName = "ORCHESTRATOR_SOURCE_HANDOFF_CONTEXT";
const sourceRequiredWorkName = "ORCHESTRATOR_SOURCE_REQUIRED_BRANCH_WORK";

assert.match(orchestratorPrompt, /\$\{ORCHESTRATOR_SOURCE_HANDOFF_CONTEXT\}/);
assert.match(orchestratorPrompt, /\$\{ORCHESTRATOR_SOURCE_REQUIRED_BRANCH_WORK\}/);
assert.ok(
readSupplementalPromptVarNames(runSource).has(sourceContextName),
`${sourceContextName} must be allowlisted for runtime prompt rendering`,
);
assert.ok(
readSupplementalPromptVarNames(runSource).has(sourceRequiredWorkName),
`${sourceRequiredWorkName} must be allowlisted for runtime prompt rendering`,
);
});

test("workflow docs cover hosted auth and self-hosting paths", () => {
Expand Down
134 changes: 134 additions & 0 deletions .agent/src/__tests__/handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
buildReviewFixPrHandoffContext,
buildHandoffDedupeKey,
buildHandoffMarker,
deriveReviewRequiredBranchWork,
decideHandoff,
defaultFixPrHandoffContext,
extractRequiredReviewActionItems,
extractReviewConclusion,
extractReviewRecommendedNextStep,
extractReviewActionItems,
Expand Down Expand Up @@ -549,6 +551,77 @@ test("review HUMAN_DECISION stops when self-approval is disabled", () => {
assert.match(decision.reason, /HUMAN_DECISION/);
});

test("review NO_AUTOMATED_ACTION dispatches self-approval for minor issues with no required branch work", () => {
const decision = decideHandoff({
automationMode: "heuristics",
sourceAction: "review",
sourceConclusion: "MINOR_ISSUES",
sourceRecommendedNextStep: "NO_AUTOMATED_ACTION",
sourceRequiredBranchWork: "false",
targetNumber: "99",
currentRound: 2,
maxRounds: 5,
allowSelfApprove: true,
});

assert.equal(decision.decision, "dispatch");
assert.equal(decision.nextAction, "agent-self-approve");
assert.equal(decision.targetNumber, "99");
assert.match(decision.reason, /no required branch-change work/);
});

test("review NO_AUTOMATED_ACTION stops cleanly when self-approval is disabled", () => {
const decision = decideHandoff({
automationMode: "heuristics",
sourceAction: "review",
sourceConclusion: "MINOR_ISSUES",
sourceRecommendedNextStep: "NO_AUTOMATED_ACTION",
sourceRequiredBranchWork: "false",
targetNumber: "99",
currentRound: 2,
maxRounds: 5,
allowSelfApprove: false,
});

assert.equal(decision.decision, "stop");
assert.equal(decision.nextAction, undefined);
assert.match(decision.reason, /self-approval disabled/);
});

test("review NO_AUTOMATED_ACTION does not self-approve blocking or required-work outcomes", () => {
const requiredWork = decideHandoff({
automationMode: "heuristics",
sourceAction: "review",
sourceConclusion: "MINOR_ISSUES",
sourceRecommendedNextStep: "NO_AUTOMATED_ACTION",
sourceRequiredBranchWork: "true",
sourceHandoffContext: "Address the required review action item.",
targetNumber: "99",
currentRound: 2,
maxRounds: 5,
allowSelfApprove: true,
});
assert.equal(requiredWork.decision, "dispatch");
assert.equal(requiredWork.nextAction, "fix-pr");
assert.equal(requiredWork.handoffContext, "Address the required review action item.");
assert.match(requiredWork.reason, /require branch changes/);

const blocking = decideHandoff({
automationMode: "heuristics",
sourceAction: "review",
sourceConclusion: "NEEDS_REWORK",
sourceRecommendedNextStep: "NO_AUTOMATED_ACTION",
sourceRequiredBranchWork: "false",
targetNumber: "99",
currentRound: 2,
maxRounds: 5,
allowSelfApprove: true,
});
assert.equal(blocking.decision, "stop");
assert.equal(blocking.nextAction, undefined);
assert.match(blocking.reason, /NO_AUTOMATED_ACTION/);
});

test("agent mode validates review HUMAN_DECISION self-approval handoff", () => {
const allowed = decideHandoff({
automationMode: "agent",
Expand Down Expand Up @@ -583,6 +656,46 @@ test("agent mode validates review HUMAN_DECISION self-approval handoff", () => {
assert.match(wrong.reason, /policy only allows agent-self-approve/);
});

test("agent mode validates review NO_AUTOMATED_ACTION self-approval handoff", () => {
const allowed = decideHandoff({
automationMode: "agent",
sourceAction: "review",
sourceConclusion: "MINOR_ISSUES",
sourceRecommendedNextStep: "NO_AUTOMATED_ACTION",
sourceRequiredBranchWork: "false",
targetNumber: "99",
currentRound: 2,
maxRounds: 5,
allowSelfApprove: true,
plannerDecision: {
decision: "handoff",
nextAction: "agent-self-approve",
reason: "No required branch-change work remains and self-approval is enabled.",
},
});
assert.equal(allowed.decision, "dispatch");
assert.equal(allowed.nextAction, "agent-self-approve");

const requiredWork = decideHandoff({
automationMode: "agent",
sourceAction: "review",
sourceConclusion: "MINOR_ISSUES",
sourceRecommendedNextStep: "NO_AUTOMATED_ACTION",
sourceRequiredBranchWork: "true",
targetNumber: "99",
currentRound: 2,
maxRounds: 5,
allowSelfApprove: true,
plannerDecision: {
decision: "handoff",
nextAction: "agent-self-approve",
reason: "Try to approve despite required branch work.",
},
});
assert.equal(requiredWork.decision, "stop");
assert.match(requiredWork.reason, /policy only allows fix-pr/);
});

test("review fix-pr handoffs preserve derived source context", () => {
const decision = decideHandoff({
automationMode: "heuristics",
Expand Down Expand Up @@ -945,3 +1058,24 @@ test("review fix-pr context extracts unchecked review synthesis action items", (
].join("\n"),
);
});

test("review required branch-work extraction ignores no-op action item sentinels", () => {
const noWork = [
"## Action Items",
"- [ ] No required branch-change work remains.",
].join("\n");
assert.deepEqual(extractReviewActionItems(noWork), ["No required branch-change work remains."]);
assert.deepEqual(extractRequiredReviewActionItems(noWork), []);
assert.equal(deriveReviewRequiredBranchWork(noWork), "false");

const required = [
"## Action Items",
"- [ ] Update the self-approval provenance check.",
"- [ ] No required branch-change work remains.",
].join("\n");
assert.deepEqual(extractRequiredReviewActionItems(required), [
"Update the self-approval provenance check.",
]);
assert.equal(deriveReviewRequiredBranchWork(required), "true");
assert.equal(deriveReviewRequiredBranchWork("## Review\nNo action section."), "unknown");
});
Loading
Loading