Skip to content

Commit db8855d

Browse files
jwaldripclaude
andauthored
fix(orchestrator): prevent per-unit MR creation in discrete mode (#232)
* fix(orchestrator): prevent per-unit MR creation in discrete mode Three interacting bugs caused agents to open one MR per unit instead of one per stage: 1. mergeUnitWorktree left stale unit branches on remote after local merge — agents saw them and opened redundant MRs 2. external_review_requested gave no branching guidance — agents had to guess which branch to MR and guessed wrong 3. Discrete mode forced ALL gates to external, overriding review:auto stages the studio author explicitly trusted to auto-advance Fixes: clean up remote unit branches after merge, tell the agent exactly which branch to MR (stage→main, ONE MR), and let discrete mode affect branching strategy without overriding review semantics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix biome formatting on template literal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(orchestrator): VCS-aware instructions and final delivery PR/MR Agent-facing instructions now respect the storage provider: - intent_complete: in git mode, instructs agent to open ONE MR from haiku/{slug}/main to project mainline for final delivery; in filesystem mode, just reports completion - external_review_requested: branch-specific MR instructions gated behind isGitRepo(), with neutral fallback for filesystem mode - Review subagents: git diff step gated behind isGitRepo(), uses getMainlineBranch() instead of hardcoded "main" - start_units announcement: "worktree" language only in git mode - Subagent commit instructions: explicit isGitRepo() guard - awaiting_external_review: removed git implementation details - Tool descriptions (haiku_review, haiku_repair): VCS-neutral - external_review_url schema: removed "PR, MR" framing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(orchestrator): remove stale comment about non-discrete mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6f055bb commit db8855d

File tree

3 files changed

+35
-30
lines changed

3 files changed

+35
-30
lines changed

packages/haiku/src/git-worktree.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ export function mergeUnitWorktree(
724724

725725
tryRun(["git", "worktree", "remove", worktreePath, "--force"])
726726
tryRun(["git", "branch", "-d", unitBranch])
727+
tryRun(["git", "push", "origin", "--delete", unitBranch])
727728

728729
if (pushFailed) {
729730
return {

packages/haiku/src/orchestrator.ts

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
createIntentBranch,
2828
createStageBranch,
2929
createUnitWorktree,
30+
getMainlineBranch,
3031
isBranchMerged,
3132
isOnIntentBranch,
3233
isOnStageBranch,
@@ -1441,7 +1442,7 @@ export function runNext(slug: string): OrchestratorAction {
14411442

14421443
// All units valid — either auto-advance or open review gate before execution.
14431444
//
1444-
// For stages with review: auto (and non-discrete mode), skip the gate
1445+
// For stages with review: auto, skip the gate
14451446
// entirely and advance directly to execution. This is critical for
14461447
// autonomous workflows where the user should not be interrupted.
14471448
//
@@ -1452,11 +1453,11 @@ export function runNext(slug: string): OrchestratorAction {
14521453
// with intent_review context until intent_reviewed is set to true.
14531454
const intentReviewed = (intent.intent_reviewed as boolean) || false
14541455
const isIntentReview = currentStage === studioStages[0] && !intentReviewed
1455-
const intentMode = (intent.mode as string) || "continuous"
14561456
const stageReviewType = resolveStageReview(studio, currentStage)
14571457

1458-
// Auto gates: skip review UI and advance directly to execution
1459-
if (stageReviewType === "auto" && intentMode !== "discrete") {
1458+
// Auto gates: skip review UI and advance directly to execution.
1459+
// Discrete mode affects branching strategy, not review type semantics.
1460+
if (stageReviewType === "auto") {
14601461
if (isIntentReview) {
14611462
setFrontmatterField(intentFile, "intent_reviewed", true)
14621463
gitCommitState(`haiku: intent ${slug} auto-approved`)
@@ -1696,17 +1697,13 @@ export function runNext(slug: string): OrchestratorAction {
16961697
const nextStage =
16971698
stageIdx < studioStages.length - 1 ? studioStages[stageIdx + 1] : null
16981699

1699-
// Use the intent's declared mode (not effective branch mode) to determine gate UI.
1700-
// A continuous intent with PR-isolated external-review stages should still show
1701-
// the stage's full gate options, not be forced to external-only.
1702-
const intentMode = (intent.mode as string) || "continuous"
17031700
const gitAvailable = isGitRepo()
17041701

17051702
// Auto gates: advance without user interaction.
17061703
// "auto" review type means the studio author trusts the FSM to advance
1707-
// without human approval. In continuous/hybrid mode, skip the gate UI
1708-
// entirely. Discrete mode always uses external review (PR per stage).
1709-
if (reviewType === "auto" && intentMode !== "discrete") {
1704+
// without human approval. Skip the gate UI entirely regardless of mode —
1705+
// discrete mode affects branching strategy, not review type semantics.
1706+
if (reviewType === "auto") {
17101707
emitTelemetry("haiku.gate.auto_advanced", {
17111708
intent: slug,
17121709
stage: currentStage,
@@ -1745,9 +1742,6 @@ export function runNext(slug: string): OrchestratorAction {
17451742
.filter((t) => t !== "external")
17461743
.join(",")
17471744
effectiveGateType = remaining || "ask"
1748-
} else if (intentMode === "discrete") {
1749-
// Pure discrete intent: always submit for external review (PR per stage)
1750-
effectiveGateType = "external"
17511745
} else if (reviewType === "ask") {
17521746
effectiveGateType = "ask"
17531747
} else if (reviewType === "await") {
@@ -2366,8 +2360,9 @@ function enrichActionWithPreview(action: OrchestratorAction): void {
23662360
case "start_units": {
23672361
const units = (action.units as string[]) || []
23682362
tell_user = `Starting ${units.length} units in parallel: ${units.join(", ")}.`
2369-
next_step =
2370-
"Each unit runs in its own worktree. After all complete, the next wave starts or we advance to review."
2363+
next_step = isGitRepo()
2364+
? "Each unit runs in its own worktree. After all complete, the next wave starts or we advance to review."
2365+
: "After all units complete, the next wave starts or we advance to review."
23712366
break
23722367
}
23732368

@@ -3210,9 +3205,11 @@ function buildRunInstructions(
32103205
)
32113206
if (wt) {
32123207
unitInstrLines.push(`${unitStep++}. Work in worktree: \`${wt}\``)
3213-
unitInstrLines.push(
3214-
`${unitStep++}. Commit frequently: \`git add -A && git commit -m "..."\`. Do NOT push.`,
3215-
)
3208+
if (isGitRepo()) {
3209+
unitInstrLines.push(
3210+
`${unitStep++}. Commit frequently: \`git add -A && git commit -m "..."\`. Do NOT push.`,
3211+
)
3212+
}
32163213
}
32173214
unitInstrLines.push(
32183215
`${unitStep++}. Call \`haiku_unit_advance_hat { intent: "${slug}", unit: "${unitName}" }\` when done`,
@@ -3293,7 +3290,7 @@ function buildRunInstructions(
32933290
)
32943291
for (const [name, content] of Object.entries(agents)) {
32953292
sections.push(
3296-
`#### Subagent: \`${name}\`\n\n<subagent tool="Task">\n## Adversarial Review: ${name}\n\nYou are the "${name}" review agent for stage "${stage}" of intent "${slug}".\n\n## Your Mandate\n\n${content}\n\n## Instructions\n\n1. Run \`git diff main...HEAD\` to get the current diff\n2. Read the stage's output artifacts in \`.haiku/intents/${slug}/stages/${stage}/\`\n3. Review through your mandate's lens\n4. Report findings as: severity (HIGH/MEDIUM/LOW), file, line, description\n5. HIGH findings MUST be fixed before the stage can advance\n</subagent>\n`,
3293+
`#### Subagent: \`${name}\`\n\n<subagent tool="Task">\n## Adversarial Review: ${name}\n\nYou are the "${name}" review agent for stage "${stage}" of intent "${slug}".\n\n## Your Mandate\n\n${content}\n\n## Instructions\n\n${isGitRepo() ? `1. Run \`git diff ${getMainlineBranch()}...HEAD\` to get the current diff\n2. Read the stage's output artifacts in \`.haiku/intents/${slug}/stages/${stage}/\`\n3. Review through your mandate's lens\n4. Report findings as: severity (HIGH/MEDIUM/LOW), file, line, description\n5. HIGH findings MUST be fixed before the stage can advance` : `1. Read the stage's output artifacts in \`.haiku/intents/${slug}/stages/${stage}/\`\n2. Review through your mandate's lens\n3. Report findings as: severity (HIGH/MEDIUM/LOW), file, line, description\n4. HIGH findings MUST be fixed before the stage can advance`}\n</subagent>\n`,
32973294
)
32983295
}
32993296
}
@@ -3324,9 +3321,16 @@ function buildRunInstructions(
33243321
}
33253322

33263323
case "intent_complete": {
3327-
sections.push(
3328-
`## Intent Complete\n\nAll stages are done for intent "${slug}". The orchestrator has marked it as completed.\n\n### Instructions\n\nReport completion summary. Suggest /haiku:gate-review then PR creation.`,
3329-
)
3324+
if (isGitRepo()) {
3325+
const mainline = getMainlineBranch()
3326+
sections.push(
3327+
`## Intent Complete\n\nAll stages are done for intent "${slug}". The orchestrator has marked it as completed.\n\n### Instructions\n\n1. Report completion summary to the user\n2. Open ONE merge request from branch \`haiku/${slug}/main\` to \`${mainline}\` for final delivery\n3. Include the H·AI·K·U browse link in the description so reviewers can see the intent, units, and knowledge artifacts\n4. Record the review URL via \`haiku_run_next { intent: "${slug}", external_review_url: "<url>" }\``,
3328+
)
3329+
} else {
3330+
sections.push(
3331+
`## Intent Complete\n\nAll stages are done for intent "${slug}". The orchestrator has marked it as completed.\n\n### Instructions\n\nReport completion summary to the user.`,
3332+
)
3333+
}
33303334
break
33313335
}
33323336

@@ -3413,7 +3417,7 @@ function buildRunInstructions(
34133417
externalUrl
34143418
? `The stage is awaiting external review at: ${externalUrl}`
34153419
: "The stage is awaiting external review but no review URL has been recorded."
3416-
}\n\nThe orchestrator checks for approval automatically (branch merge detection + URL-based CLI probing). Neither detected approval yet.\n\nInform the user that the stage is waiting on external review. After the review is approved, run \`/haiku:pickup\` to continue.`,
3420+
}\n\nThe orchestrator checks for approval automatically. Neither detected approval yet.\n\nInform the user that the stage is waiting on external review. After the review is approved, run \`/haiku:pickup\` to continue.`,
34173421
)
34183422
break
34193423
}
@@ -3553,8 +3557,7 @@ export const orchestratorToolDefs = [
35533557
intent: { type: "string", description: "Intent slug" },
35543558
external_review_url: {
35553559
type: "string",
3556-
description:
3557-
"URL where stage was submitted for external review (PR, MR, etc.)",
3560+
description: "URL where stage was submitted for external review",
35583561
},
35593562
},
35603563
required: ["intent"],
@@ -3940,8 +3943,9 @@ export async function handleOrchestratorTool(
39403943
intent: slug,
39413944
stage,
39423945
feedback: reviewResult.feedback,
3943-
message:
3944-
"External review requested. Submit the work for review through your project's review process (PR, MR, review board, etc.). Include the H·AI·K·U browse link in the description so reviewers can see the intent, units, and knowledge artifacts. Record the review URL via haiku_run_next { intent, external_review_url }. Run /haiku:pickup again after approval.",
3946+
message: isGitRepo()
3947+
? `External review requested. Open ONE merge request from branch 'haiku/${slug}/${stage}' to 'haiku/${slug}/main'. Do NOT open separate MRs for individual units — all unit work is already merged into the stage branch. Include the H·AI·K·U browse link in the description so reviewers can see the intent, units, and knowledge artifacts. Record the review URL via haiku_run_next { intent, external_review_url }. Run /haiku:pickup again after approval.`
3948+
: `External review requested. Submit the work for review through your project's review process. Record the review URL via haiku_run_next { intent, external_review_url }. Run /haiku:pickup again after approval.`,
39453949
}
39463950
return text(withInstructions(gateResult))
39473951
}

packages/haiku/src/state-tools.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,7 +2284,7 @@ export const stateToolDefs = [
22842284
{
22852285
name: "haiku_review",
22862286
description:
2287-
"Runs a git diff against main/upstream and returns formatted pre-delivery code review instructions with diff, stats, review guidelines, and review-agent config.",
2287+
"Returns formatted pre-delivery code review instructions with diff, stats, review guidelines, and review-agent config.",
22882288
inputSchema: {
22892289
type: "object" as const,
22902290
properties: {
@@ -2344,7 +2344,7 @@ export const stateToolDefs = [
23442344
{
23452345
name: "haiku_repair",
23462346
description:
2347-
"Scan intents for metadata issues and auto-apply safe fixes. In a git repo, the default behavior is to scan ALL `haiku/<slug>/main` intent branches sequentially via temporary worktrees, auto-apply safe fixes (overlong title trim, legacy field renames, missing defaults, studio alias migration), commit and push the fixes to each branch, and open a PR/MR back to mainline if the branch was already merged. Pass `intent` to repair a single intent in the current working directory only. Pass `skip_branches: true` to force cwd-only mode in a git repo. Pass `apply: false` to scan without applying fixes.",
2347+
"Scan intents for metadata issues and auto-apply safe fixes. In a git repo, scans all intent branches sequentially, auto-applies safe fixes, syncs changes, and opens PRs/MRs for already-merged branches. In filesystem mode, scans intents in the current working directory. Pass `intent` to repair a single intent only. Pass `skip_branches: true` to force cwd-only mode in a git repo. Pass `apply: false` to scan without applying fixes.",
23482348
inputSchema: {
23492349
type: "object" as const,
23502350
properties: {

0 commit comments

Comments
 (0)