fix(orchestration): keep optional preamble sections out of Markdown headings

The sub-dispatch and base-drift sections end with a bare rule directly under a
paragraph, which Markdown parses as a setext H2, so the Chat UI rendered the
section's last sentence as a heading. The unfenced sub-dispatch commands also
lost their angle-bracket placeholders to the raw-HTML pass. Fence those
commands like the main CLI block and put a blank line before each closing rule.
This commit is contained in:
Merge Sim
2026-09-04 15:04:10 -07:00
parent 68ddeef063
commit 2f3a614ea4
3 changed files with 41 additions and 4 deletions
@@ -132,7 +132,7 @@ export async function dispatchTaskToWorker(params: {
let gateContext = ''
if (gates.length > 0) {
const latest = gates.at(-1)!
gateContext = `\n\n--- DECISION GATE RESOLVED ---\nQuestion: ${latest.question}\nResolution: ${latest.resolution}\n---\n`
gateContext = `\n\n--- DECISION GATE RESOLVED ---\nQuestion: ${latest.question}\nResolution: ${latest.resolution}\n\n---\n`
}
try {
@@ -31,6 +31,16 @@ function cliFence(result: string): string {
return match?.[1] ?? ''
}
function markdownBlocks(result: string) {
const tree = unified().use(remarkParse).parse(result)
return {
headings: tree.children.filter((node) => node.type === 'heading'),
codeBlocks: tree.children.filter((node) => node.type === 'code')
}
}
const driftParams = { base: 'origin/main', behind: 3, recentSubjects: ['fix: a', 'feat: b'] }
describe('buildDispatchPreamble', () => {
it('substitutes template variables', () => {
const result = buildDispatchPreamble(baseParams())
@@ -73,15 +83,36 @@ describe('buildDispatchPreamble', () => {
it('fences shell comments so Markdown does not promote them to headings', () => {
const result = buildDispatchPreamble(baseParams())
const tree = unified().use(remarkParse).parse(result)
const headings = tree.children.filter((node) => node.type === 'heading')
const codeBlocks = tree.children.filter((node) => node.type === 'code')
const { headings, codeBlocks } = markdownBlocks(result)
expect(headings).toHaveLength(0)
expect(codeBlocks).toHaveLength(1)
expect(codeBlocks[0]).toMatchObject({ lang: 'sh', value: cliFence(result) })
})
// Why: a `---` rule directly under a paragraph is a setext H2, so the optional
// sections' closing rules must not turn their last sentence into a heading.
it('renders no Markdown headings when the sub-dispatch and drift sections are present', () => {
const result = buildDispatchPreamble(
baseParams({ canDispatchSubWorkers: true, baseDrift: driftParams })
)
const { headings, codeBlocks } = markdownBlocks(result)
expect(headings).toHaveLength(0)
expect(codeBlocks).toHaveLength(2)
expect(codeBlocks[1]).toMatchObject({ lang: 'sh' })
expect(codeBlocks[1].value).toContain('orchestration worker-start --task <task_id>')
expect(result).toContain('able to dispatch further.\n\n---')
expect(result).toContain('before starting.\n\n---')
})
it('sub-dispatch fence passes bash -n', { timeout: 15_000 }, () => {
const result = buildDispatchPreamble(baseParams({ canDispatchSubWorkers: true }))
const { codeBlocks } = markdownBlocks(result)
const check = spawnSync('bash', ['-n'], { input: codeBlocks[1].value, encoding: 'utf8' })
expect(check.status).toBe(0)
})
it('includes heartbeat CLI block with taskId and dispatchId and 5-minute cadence', () => {
const result = buildDispatchPreamble(baseParams())
expect(result).toContain('--type heartbeat')
@@ -196,6 +196,8 @@ work under the new Dispatch; ignore stale follow-ups from the settled task.`
// Why the whole section is omitted rather than softened when nesting is off: a
// worker told it "usually cannot" delegate still tries, then reports the refusal
// as a blocker.
// Why fenced + blank line before the closing `---`: unfenced `<placeholders>` are stripped as raw
// HTML by the Chat UI, and a rule directly under a paragraph is a setext H2 (giant last sentence).
function buildSubDispatchSection(cli: string): string {
return `
@@ -203,13 +205,16 @@ function buildSubDispatchSection(cli: string): string {
You may dispatch sub-workers for this task. Bind your own Run first, then create
and start each one:
\`\`\`sh
${cli} orchestration run-create --objective "<what the sub-workers are for>" --json
${cli} orchestration task-create --spec "<sub-task>" --json
${cli} orchestration worker-start --task <task_id> --worktree current --agent <agent> --json
\`\`\`
You own those sub-workers: wait for their worker_done, and do not report your own
until they have settled. Nesting is capped, so a sub-worker of yours may not be
able to dispatch further.
---`
}
@@ -224,5 +229,6 @@ ${subjects}
If any look relevant to your task, either pull them in (\`git pull --rebase
${drift.base}\` or equivalent) or escalate to the coordinator before starting.
---`
}