Skip to content

Commit 2cb824c

Browse files
committed
merge: resolve orchestrator.ts conflict with main
Keep our 7 new buildRunInstructions cases and main's new safe_intent_repair case. Both sets of changes are additive to the switch statement. https://claude.ai/code/session_01WtQrEmTYmoUheaP49HAei7
2 parents d5de52d + 5222567 commit 2cb824c

File tree

9 files changed

+971
-42
lines changed

9 files changed

+971
-42
lines changed

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"metadata": {
88
"description": "H·AI·K·U — universal lifecycle orchestration with hat-based workflows, completion criteria, and automatic context preservation.",
9-
"version": "1.101.6"
9+
"version": "1.101.7"
1010
},
1111
"plugins": [
1212
{

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.101.7] - 2026-04-15
9+
10+
### Fixed
11+
- External review gates now reliably coordinate with external review systems for seamless stage advancement.
12+
813
## [1.101.6] - 2026-04-14
914

1015
### Fixed

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/haiku/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"test:server": "npx tsx test/server-tools.test.mjs"
1919
},
2020
"dependencies": {
21+
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
2122
"@modelcontextprotocol/sdk": "^1.28.0",
2223
"@sentry/node": "^10.47.0",
2324
"gray-matter": "^4.0.3",

packages/haiku/src/orchestrator.ts

Lines changed: 244 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -959,26 +959,170 @@ export function runNext(slug: string): OrchestratorAction {
959959
}
960960

961961
// Consistency check: verify all stages before active_stage are completed.
962-
// If not, reset to the first incomplete stage. This catches stale active_stage
963-
// values set by old binaries or direct file edits.
962+
// If not, either synthesize completion records (safe repair) or reset to
963+
// the first incomplete stage. Safe repair triggers when the active stage
964+
// has real work (units) — this indicates a migrated intent where earlier
965+
// stages were never elaborated. Resetting backwards would force
966+
// re-elaboration of empty stages while real work sits in a later stage.
964967
const activeIdx = studioStages.indexOf(currentStage)
965968
if (activeIdx > 0) {
969+
// Collect all incomplete prior stages in one pass
970+
const incompletePrior: string[] = []
966971
for (let i = 0; i < activeIdx; i++) {
967972
const prevState = readJson(
968973
join(iDir, "stages", studioStages[i], "state.json"),
969974
)
970975
const prevStatus = (prevState.status as string) || "pending"
971976
if (prevStatus !== "completed") {
972-
// Found an incomplete stage before active_stage — reset
973-
currentStage = studioStages[i]
974-
// Fix the intent's active_stage to match reality
977+
incompletePrior.push(studioStages[i])
978+
}
979+
}
980+
981+
if (incompletePrior.length > 0) {
982+
// Check if the active stage has real work — units on disk
983+
const activeUnitsDir = join(iDir, "stages", currentStage, "units")
984+
const activeUnitFiles = existsSync(activeUnitsDir)
985+
? readdirSync(activeUnitsDir).filter((f) => f.endsWith(".md"))
986+
: []
987+
988+
if (activeUnitFiles.length > 0) {
989+
// ── Safe intent repair ──────────────────────────────────────
990+
// The active stage has real work but earlier stages are incomplete.
991+
// This is a migration artifact (e.g., AIDLC → H·AI·K·U migration
992+
// that only populated the development stage). Synthesize completion
993+
// records for incomplete prior stages so the FSM can proceed without
994+
// forcing re-elaboration of empty stages.
995+
//
996+
// Safety constraints:
997+
// 1. Only synthesizes for stages with NO units (truly empty)
998+
// — stages with units but incomplete status are left for manual review
999+
// 2. Uses the same completion record format as haiku_repair
1000+
// 3. The agent cannot trigger this — it's FSM-internal
1001+
// 4. No hook bypass — this runs inside haiku_run_next
1002+
1003+
const synthesized: string[] = []
1004+
const needsManualReview: string[] = []
1005+
const now = timestamp()
1006+
const intentStarted =
1007+
(intent.started_at as string) || (intent.created_at as string) || now
1008+
1009+
for (const stageName of incompletePrior) {
1010+
const priorUnitsDir = join(iDir, "stages", stageName, "units")
1011+
const priorUnitFiles = existsSync(priorUnitsDir)
1012+
? readdirSync(priorUnitsDir).filter((f) => f.endsWith(".md"))
1013+
: []
1014+
1015+
if (priorUnitFiles.length > 0) {
1016+
// Stage has units but isn't completed — this needs manual attention
1017+
needsManualReview.push(stageName)
1018+
} else {
1019+
// Truly empty prior stage — safe to synthesize completion
1020+
const stageDir = join(iDir, "stages", stageName)
1021+
mkdirSync(stageDir, { recursive: true })
1022+
const statePath = join(stageDir, "state.json")
1023+
writeJson(statePath, {
1024+
stage: stageName,
1025+
status: "completed",
1026+
phase: "gate",
1027+
started_at: intentStarted,
1028+
completed_at: intentStarted,
1029+
gate_entered_at: null,
1030+
gate_outcome: "advanced",
1031+
})
1032+
synthesized.push(stageName)
1033+
}
1034+
}
1035+
1036+
// Check if the active stage's units need input backfill.
1037+
// If the stage is in execute phase but units lack inputs, regress
1038+
// to elaborate so the normal backpressure can enforce input declarations.
1039+
const activeStageState = readJson(
1040+
join(iDir, "stages", currentStage, "state.json"),
1041+
)
1042+
const activePhase = (activeStageState.phase as string) || ""
1043+
let phaseRegressed = false
1044+
const missingInputs: string[] = []
1045+
if (activePhase === "execute") {
1046+
for (const f of activeUnitFiles) {
1047+
const fm = readFrontmatter(join(activeUnitsDir, f))
1048+
const unitStatus = (fm.status as string) || ""
1049+
if (["completed", "skipped", "failed"].includes(unitStatus))
1050+
continue
1051+
const inputs =
1052+
(fm.inputs as string[]) || (fm.refs as string[]) || []
1053+
if (inputs.length === 0) missingInputs.push(f)
1054+
}
1055+
if (missingInputs.length > 0) {
1056+
// Regress phase to elaborate so validateUnitInputs catches this
1057+
activeStageState.phase = "elaborate"
1058+
writeJson(
1059+
join(iDir, "stages", currentStage, "state.json"),
1060+
activeStageState,
1061+
)
1062+
phaseRegressed = true
1063+
}
1064+
}
1065+
1066+
if (synthesized.length > 0 || phaseRegressed) {
1067+
gitCommitState(
1068+
`haiku: safe-repair ${slug} — synthesize ${synthesized.join(", ")}${phaseRegressed ? "; regress phase to elaborate" : ""}`,
1069+
)
1070+
}
1071+
1072+
emitTelemetry("haiku.fsm.safe_repair", {
1073+
intent: slug,
1074+
active_stage: currentStage,
1075+
synthesized_stages: synthesized.join(","),
1076+
needs_manual_review: needsManualReview.join(","),
1077+
phase_regressed: String(phaseRegressed),
1078+
})
1079+
1080+
// If all incomplete stages were synthesized, proceed normally
1081+
// by falling through to the rest of runNext. If any need manual
1082+
// review, return an action so the agent can report the situation.
1083+
if (needsManualReview.length > 0) {
1084+
return {
1085+
action: "safe_intent_repair",
1086+
intent: slug,
1087+
studio,
1088+
stage: currentStage,
1089+
synthesized_stages: synthesized,
1090+
needs_manual_review: needsManualReview,
1091+
phase_regressed: phaseRegressed,
1092+
units_missing_inputs: missingInputs,
1093+
message: `Intent '${slug}' was in an inconsistent state — work exists in '${currentStage}' but earlier stages were incomplete.\n\n${synthesized.length > 0 ? `Synthesized completion records for empty stages: [${synthesized.join(", ")}]\n` : ""}Stages needing manual review (have units but aren't completed): [${needsManualReview.join(", ")}]\n${phaseRegressed ? `\nAdditionally, phase was regressed from 'execute' to 'elaborate' because some units are missing \`inputs:\` declarations.\n` : ""}Resolve these stages manually, then call haiku_run_next again.`,
1094+
}
1095+
}
1096+
1097+
// All prior stages synthesized — if phase was regressed, let the
1098+
// agent know so it can address missing inputs before execution.
1099+
// Otherwise fall through to normal processing.
1100+
if (phaseRegressed) {
1101+
return {
1102+
action: "safe_intent_repair",
1103+
intent: slug,
1104+
studio,
1105+
stage: currentStage,
1106+
synthesized_stages: synthesized,
1107+
needs_manual_review: [],
1108+
phase_regressed: true,
1109+
units_missing_inputs: missingInputs,
1110+
message: `Intent '${slug}' repaired — synthesized completion for [${synthesized.join(", ")}]. Phase regressed from 'execute' to 'elaborate' because some units are missing \`inputs:\` declarations. Add inputs to the flagged units, then call haiku_run_next to proceed.`,
1111+
}
1112+
}
1113+
1114+
// Clean repair with no phase regression — fall through to normal
1115+
// runNext processing. The agent doesn't need to take special action.
1116+
} else {
1117+
// No units in the active stage — normal consistency reset.
1118+
// The intent may have been corrupted or active_stage set incorrectly.
1119+
currentStage = incompletePrior[0]
9751120
setFrontmatterField(intentFile, "active_stage", currentStage)
9761121
emitTelemetry("haiku.fsm.consistency_fix", {
9771122
intent: slug,
9781123
stale_stage: activeStage,
9791124
corrected_stage: currentStage,
9801125
})
981-
break
9821126
}
9831127
}
9841128
}
@@ -3067,6 +3211,24 @@ function buildRunInstructions(
30673211
break
30683212
}
30693213

3214+
case "safe_intent_repair": {
3215+
const synthesizedStages = (action.synthesized_stages as string[]) || []
3216+
const phaseWasRegressed = (action.phase_regressed as boolean) || false
3217+
sections.push(`## Safe Intent Repair\n\n${action.message}`)
3218+
if (synthesizedStages.length > 0) {
3219+
sections.push(`**Synthesized stages:** ${synthesizedStages.join(", ")}`)
3220+
}
3221+
if (phaseWasRegressed) {
3222+
sections.push(
3223+
"**Phase regressed:** The active stage was regressed from `execute` to `elaborate` because some units are missing `inputs:` declarations. Address the missing inputs before proceeding.",
3224+
)
3225+
}
3226+
sections.push(
3227+
`### Instructions\n\nResolve any stages needing manual review, then call \`haiku_run_next { intent: "${slug}" }\` again.`,
3228+
)
3229+
break
3230+
}
3231+
30703232
default: {
30713233
sections.push(
30723234
`## Unknown Action: ${action.action}\n\n${JSON.stringify(action, null, 2)}`,
@@ -3682,6 +3844,82 @@ export async function handleOrchestratorTool(
36823844
}
36833845
}
36843846

3847+
// ── Repair agent intercept ─────────────────────────────────────────
3848+
// If runNext detected a broken migrated intent, try the embedded repair
3849+
// agent before returning to the outer agent. Falls through to the normal
3850+
// withInstructions return if the agent isn't available or repair fails.
3851+
if (result.action === "safe_intent_repair") {
3852+
try {
3853+
const { runRepairAgent } = await import("./repair-agent.js")
3854+
const root = findHaikuRoot()
3855+
const iDir = join(root, "intents", slug)
3856+
3857+
// Resolve studio directory via the cached studio reader
3858+
const studioInfo = resolveStudio(intentStudio)
3859+
const studioDir = studioInfo?.path
3860+
if (!studioDir) {
3861+
// Can't find studio — fall through to normal handling
3862+
syncSessionMetadata(slug, args.state_file as string | undefined)
3863+
return text(withInstructions(result))
3864+
}
3865+
3866+
const activeStage = (result.stage as string) || ""
3867+
const diagnosis = {
3868+
slug,
3869+
intentDir: iDir,
3870+
studio: intentStudio,
3871+
studioDir,
3872+
activeStage,
3873+
synthesizedStages: (result.synthesized_stages as string[]) || [],
3874+
needsManualReview: (result.needs_manual_review as string[]) || [],
3875+
phaseRegressed: (result.phase_regressed as boolean) || false,
3876+
unitsMissingInputs: (result.units_missing_inputs as string[]) || [],
3877+
}
3878+
3879+
const repairResult = await runRepairAgent(diagnosis)
3880+
3881+
if (repairResult.success && !repairResult.fallbackUsed) {
3882+
// Repair agent succeeded — run FSM again to get the real next action
3883+
const postRepairResult = runNext(slug)
3884+
3885+
// Guard: if repair didn't actually fix things, don't loop
3886+
if (postRepairResult.action === "safe_intent_repair") {
3887+
// Fall through to return the original result as-is
3888+
} else {
3889+
emitTelemetry("haiku.orchestrator.action", {
3890+
intent: slug,
3891+
action: postRepairResult.action,
3892+
})
3893+
if (stFile)
3894+
logSessionEvent(stFile, {
3895+
event: "run_next",
3896+
intent: slug,
3897+
action: postRepairResult.action,
3898+
stage: postRepairResult.stage,
3899+
unit: postRepairResult.unit,
3900+
hat: postRepairResult.hat,
3901+
wave: postRepairResult.wave,
3902+
})
3903+
3904+
syncSessionMetadata(slug, args.state_file as string | undefined)
3905+
3906+
const repairNote = `**Intent repaired automatically:** ${repairResult.summary}\n\n---\n\n`
3907+
return {
3908+
content: [
3909+
{
3910+
type: "text" as const,
3911+
text: repairNote + withInstructions(postRepairResult),
3912+
},
3913+
],
3914+
}
3915+
}
3916+
}
3917+
// Repair failed or used fallback — fall through to return safe_intent_repair as-is
3918+
} catch {
3919+
// Repair agent not available — fall through to normal handling
3920+
}
3921+
}
3922+
36853923
syncSessionMetadata(slug, args.state_file as string | undefined)
36863924
return text(withInstructions(result))
36873925
}

0 commit comments

Comments
 (0)