mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
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:
@@ -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.
|
||||
|
||||
---`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user