Could be:
{
commandA 3<&- | perl -pe '
exec "commandB <&3 3<&-" if !$pid && /started successfully/ && ($pid = fork) == 0;
END {if ($pid) {wait; exit($? & 127 ? $? + 128 : $? >> 8)}}'
} 3<&0
If using zsh instead of bash, and GNU tee, you could do something similar with the caveat that commandB's exit status will be lost with:
{
commandA 3<&- |
tee -p >(grep -q 'started successfully' && commandB <&3 3<&-)
} 3<&0
That would also work in bash (though you'd need to add a exec in front of commandB as bash doesn't always optimise out the fork there) except for the fact that commandB would not be waited for there.
In both approaches we take care that commandA and commandB's stdin be left undisturbed. commandA's stdout is changed from the original to a pipe, but that's necessary if we have to find started successfully in there, but that output is passed along by perl -p or tee. commandB's stdout is undisturbed.
Note that some command buffer their output when stdout is not a terminal device. So with commandA's output here becoming a pipe, you might find that started successfully is only printed as part of a large block, and commandB not started as early as it could be as a result.
With zsh, it can be worked around by using zpty to start commandA with its output connected to pseudo-terminal:
zmodload zsh/zpty
# start commandA with only stdout going to the pseudo-terminal
# stdin and stderr restored.
zpty A 'command A <&3 3<&- 2>&4 4>&-' 3<&0 4>&2
pid=
while zpty -r A line; do
print -rn - $line
if (( ! pid )) && [[ $line = *"started successfully" ]]; then
{ commandB <&3 3<&- & } 3<&0
pid=$!
fi
done
(( ! pid )) || wait $pid